AI Tool Directory — Context Pack
Copyable context pack for an AI tool directory site built with Astro and PostgreSQL, so agents understand the submission flow, tagging system, and content model from prompt one.
CursorClaude CodeCodexWindsurf AstroPostgreSQLTypeScriptTailwind
Paste this at the top of a task so the agent understands the content model, moderation workflow, and build strategy for the AI tool directory before it makes any changes.
Project Background
A directory of AI tools and products. Each listing is a row in PostgreSQL,pulled at build time by Astro's Content Layer and rendered to static HTML.Submissions arrive via a public HTML form that posts to an Astro API endpoint;entries are held in a `pending` state until an admin approves them through apassword-protected dashboard. The site is deployed to Cloudflare Pages with aNeon Postgres database. Search is client-side Pagefind over the static output.Stack
Astro 5 (static output, SSR disabled except for /api/* and /admin/* routes)TypeScript (strict)Tailwind CSS v4PostgreSQL 16 via Neon (serverless HTTP driver @neondatabase/serverless)Drizzle ORM + drizzle-kit for schema + migrationsPagefind (client-side full-text search over built HTML)Cloudflare Pages (static + Functions for the hybrid API routes)Directory Structure
src/ pages/ index.astro # Homepage + featured tools grid tools/ [slug].astro # Individual tool page (generated at build time) index.astro # Browsable directory with category filters api/ submit.ts # POST — accepts public tool submissions admin/ approve.ts # POST — marks a listing approved (auth-gated) reject.ts # POST — deletes a pending listing (auth-gated) admin/ index.astro # Admin dashboard — lists pending submissions lib/ db/ index.ts # Neon client + Drizzle instance schema.ts # tools, categories, tags tables auth.ts # Simple HMAC token check for /admin routes slugify.ts # Deterministic slug generation from tool name validate.ts # Zod schema for submission form fields content/ featured/ # MDX overrides for hand-curated tool blurbs components/ ToolCard.astro CategoryFilter.astro SubmissionForm.astrodrizzle/ migrations/drizzle.config.tsCoding Conventions
- Tool listings are the source of truth in Postgres, not MDX files. MDX in src/content/featured/ only overrides the blurb for curated picks.- Slugs are derived from the tool name via src/lib/slugify.ts and stored in the DB. Never compute the slug on the fly — always read it from the DB row.- All submission input is validated with the Zod schema in src/lib/validate.ts before any DB write. Return 400 with field-level errors on failure.- The /admin/* routes check a Bearer token with the HMAC verifier in src/lib/auth.ts. Do not add another auth mechanism.- DB calls happen only in Astro API endpoints (*.ts in pages/api/) and in the getStaticPaths() of dynamic pages. Never in .astro components directly.- Pagefind is run as a post-build step (`npx pagefind --site dist`). Do not add client-side search that bypasses Pagefind.- Category and tag slugs are normalized to lowercase kebab-case. Maintain the canonical list in src/lib/db/schema.ts (categories table).AI Task Boundaries
- Do not add a full auth system (NextAuth, Better Auth) — the admin gate is intentionally a single shared HMAC token for simplicity.- Do not move tool data into MDX content collections — the DB is the source of truth and MDX is only for editorial overrides.- Do not change the Astro output mode to fully SSR — the majority of pages must remain static for Pagefind and CDN caching to work.- Do not allow unapproved (status != 'approved') tools to appear in getStaticPaths() or the public directory index.- Schema changes must go through Drizzle migrations. Never use raw ALTER TABLE against the Neon database directly.- External URLs from user submissions must be stored as-is and only rendered through rel="nofollow noopener noreferrer" links.- Do not expose the admin token in any client-side bundle, Astro component prop, or public API response.llms.txt
# AI Tool DirectoryFramework: Astro 5 (hybrid: static pages + SSR API routes)DB: Neon Postgres via Drizzle ORM (src/lib/db/schema.ts)Search: Pagefind (post-build, client-side over static HTML)Submissions: POST /api/submit → status='pending' → admin approvalAdmin auth: HMAC Bearer token (src/lib/auth.ts) — no user accountsSlugs: generated once on insert, stored in DB (src/lib/slugify.ts)Content overrides: src/content/featured/ MDX (editorial blurbs only)Deploy: Cloudflare Pages (static + Functions)