A public directory of tools and websites — Product Hunt style reference with a Notion-like table, ⌘K command palette search and per-tool detail pages. Dark-only, English-only.
Built by @BastianMR with Astro 5, React 19, Tailwind CSS 4 and shadcn/ui. Fully static output — no backend required.
- 📋 Catalog table — server-rendered rows (SEO-friendly), sortable columns.
- 🗂️ Library sidebar — categories with counts (tools can have several), multi-select tag chips.
- ⌘K Command palette — fuzzy live search over name/description/tags, keyboard-first.
- 📄 Detail pages — every tool gets its own shareable URL
/tools/<slug>; on desktop it opens as a side panel (Sheet) with full browser-history support; on mobile it navigates directly. - 🔗 Shareable filters — active category/tags sync to the URL (
?category=design&tags=notes). - 🌗 Dark-only design — consistent shadcn/ui dark theme, no theme switching.
- ✅ Validated content — one markdown file per tool; the build fails naming file + field if data is invalid. Bad data never ships.
- 🔍 SEO ready — sitemap, RSS feed, per-page Open Graph tags.
git clone https://github.com/BastianMR/OpenTools.git
cd OpenTools
npm install
npm run dev # http://localhost:4321You'll see 6 demo tools. Replace them with your own (next section) or keep exploring.
Create src/content/tools/<slug>.md (slug = kebab-case URL):
---
name: "Excalidraw" # required
url: "https://excalidraw.com" # required, must be http(s)
categories: ["design", "tools"] # required, one or more
added: 2026-08-22 # required, ISO date
description: "..." # required, max 500 chars
tags: ["diagrams"] # optional
comment: "..." # optional, your personal take
---Commit to main → deploy runs automatically. If a field is invalid the build fails naming the
file and field. Full contract: specs/001-opentools-catalog-site/data-model.md.
| What | Where |
|---|---|
| Site name, tagline, author | src/lib/site-config.ts |
| UI strings | src/lib/i18n.ts |
| Colors / theme tokens | src/styles/global.css |
| Categories & tags | free-form in each entry's frontmatter |
More details: docs/customization.md.
- Push this repo to your GitHub account.
- In Cloudflare Dashboard: Workers & Pages → Create → Pages → Connect to Git.
- Pick the repo, framework preset Astro, build command
npm run build, outputdist/. - Save — every push to
maindeploys production; PRs get preview URLs.
Works identically on Netlify/Vercel (any static host).
No cloud needed — run it on your own machine, NAS or VPS:
docker compose up -d --buildOpen http://localhost:8080. The container serves the static build with nginx (immutable caching for hashed assets, gzip, custom 404) and restarts on reboot.
Useful commands:
docker compose logs -f # follow logs
docker compose down # stop
docker compose up -d --build # rebuild after pulling changesPrefer plain nginx/caddy? Point it at the output of npm run build (dist/) —
see nginx.conf for the recommended cache and try_files rules.
Publish entries from an n8n workflow via the GitHub API:
PUT https://api.github.com/repos/{owner}/{repo}/contents/src/content/tools/{slug}.md
Authorization: Bearer <token> # token lives ONLY in n8n credentials
{ "message": "feat(content): add {slug}", "content": "<base64 .md>", "branch": "main" }
If the frontmatter is invalid, the deploy fails but the last good build stays online.
| Command | Action |
|---|---|
npm run dev |
Dev server at localhost:4321 |
npm run build |
Production build (validates content schema) |
npm test |
Unit tests (Vitest) |
npm run test:e2e |
E2E tests (Playwright, builds first) |
npm run lint |
Type check + Prettier |
npm run graph:update |
Refresh codebase knowledge graph (graphify) |
src/
├── content/tools/*.md ← your catalog entries (the data)
├── lib/
│ ├── content-source.ts ← ONLY place that reads content (swap for a DB later)
│ ├── site-config.ts ← branding: name, tagline, author
│ ├── schema.ts ← frontmatter contract (Zod)
│ └── i18n.ts ← UI strings
├── components/
│ ├── CatalogExplorer.tsx ← interactive island (table + ⌘K + sheet panel)
│ └── ui/ ← shadcn/ui primitives
├── layouts/Base.astro ← head/SEO/header/footer
└── pages/ ← routes (/, /tools/[slug], rss.xml, 404)
The content layer is deliberately isolated: today it reads markdown files via Astro Content
Collections; swapping in Supabase/Turso/any backend later means changing exactly one file
(src/lib/content-source.ts) — no page or component changes.
Issues and PRs welcome — see CONTRIBUTING.md and
specs/ for how features are specified before they're built.
Code: MIT. Demo seed entries ship as examples; replace them with your own content.