# 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.

**Type:** Context Pack  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Astro, PostgreSQL, TypeScript, Tailwind  
**Updated:** 2026-06-08

---

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

```txt
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 a
password-protected dashboard. The site is deployed to Cloudflare Pages with a
Neon Postgres database. Search is client-side Pagefind over the static output.
```

## Stack

```txt
Astro 5 (static output, SSR disabled except for /api/* and /admin/* routes)
TypeScript (strict)
Tailwind CSS v4
PostgreSQL 16 via Neon (serverless HTTP driver @neondatabase/serverless)
Drizzle ORM + drizzle-kit for schema + migrations
Pagefind (client-side full-text search over built HTML)
Cloudflare Pages (static + Functions for the hybrid API routes)
```

## Directory Structure

```txt
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.astro
drizzle/
  migrations/
drizzle.config.ts
```

## Coding Conventions

```txt
- 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

```txt
- 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

```txt
# AI Tool Directory
Framework: 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 approval
Admin auth: HMAC Bearer token (src/lib/auth.ts) — no user accounts
Slugs: 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)
```