# Project structure

> The starter's directories and files: what each one is for, where to make a given change, and what the free edition keeps. Generated from the repository and AGENTS.md.

You are reading one page of the ZeroDirs documentation. ZeroDirs is a paid Astro + Cloudflare Workers starter for directory sites: every page is rendered to static HTML at build time, the site ships no client JavaScript outside `/search/`, and one file — `site.config.ts` — carries roughly 80% of the customisation.

Two things to hold on to before you act on anything below:

- `site.config.ts` is validated by a zod schema with ten cross-field rules. After any edit to it, run `pnpm check:config`; every problem is reported as `site.config.ts › <path>: <message>` and the whole file is checked at once.
- The repository ships its own `AGENTS.md` with twenty hard rules, and a machine-checked test suite behind them. If you are working inside a ZeroDirs project, read that file first — it overrides anything general you infer from this page.

Source: https://zerodirs.com/docs/start/project-structure/

---

<!-- DO NOT EDIT — generated from zerodirs/zerodirs AGENTS.md and git ls-files by .github/docs/generate.ts (docs-sync.yml). Edit the source, not this page. -->

The tree is what git tracks in the starter today, with `src/`, `src/pages/` and `.github/` opened one level. The table under it is the map from `AGENTS.md`, the file an agent reads first, so the two never disagree. Files with `.free.` in the name are the free edition's versions of their neighbours; the release tooling swaps them in.

```text
zerodirs/
├── .agents/
├── .github/
│   ├── docs/
│   ├── release/
│   └── workflows/
├── docs/
├── drizzle/
├── integrations/
├── public/
├── scripts/
├── seed/
├── src/
│   ├── actions/
│   ├── assets/
│   ├── components/
│   ├── config/
│   ├── content/
│   ├── db/
│   ├── features/
│   ├── icons/
│   ├── islands/
│   ├── layouts/
│   ├── lib/
│   ├── loaders/
│   ├── pages/
│   │   ├── account/
│   │   ├── admin/
│   │   ├── api/
│   │   ├── auth/
│   │   ├── blog/
│   │   ├── login/
│   │   ├── og/
│   │   ├── out/
│   │   ├── submit/
│   │   ├── 404.astro
│   │   ├── [...path].astro
│   │   ├── [advertise].astro
│   │   ├── about.astro
│   │   ├── index.astro
│   │   ├── privacy.astro
│   │   ├── robots.txt.ts
│   │   ├── rss.xml.ts
│   │   ├── search.astro
│   │   └── terms.astro
│   ├── server/
│   ├── styles/
│   ├── templates/
│   ├── content.config.ts
│   ├── env.d.ts
│   ├── env.free.d.ts
│   ├── middleware.ts
│   └── worker.ts
├── tests/
├── .dev.vars.example
├── .editorconfig
├── .env.example
├── .gitignore
├── .nvmrc
├── AGENTS.free.md
├── AGENTS.md
├── astro.config.ts
├── CHANGELOG.md
├── CLAUDE.md
├── drizzle.config.ts
├── LICENSE
├── LICENSE.free
├── lighthouserc.cjs
├── lighthouserc.mobile.cjs
├── package.json
├── playwright.config.ts
├── pnpm-lock.yaml
├── README.free.md
├── README.md
├── site.config.ts
├── tsconfig.json
├── vitest.config.ts
├── vitest.workers.config.ts
├── worker-configuration.d.ts
├── wrangler.free.jsonc
└── wrangler.jsonc
```

Nothing under `node_modules/`, `dist/`, `.astro/` or `.wrangler/` is tracked: install, build, `astro check` and the dev server create those.

## Where things live

The free edition column says what the release tooling does with the path: *deleted* means the free edition does not have it; *replaced* means it has a smaller version of it.

| Path | What | Free edition |
| --- | --- | --- |
| `site.config.ts` | The one file a buyer edits: brand, routes, categories, tags, tiers, SEO, pSEO, theme, feature flags | yes |
| `astro.config.ts` | Derived from `site.config.ts`; adapter, fonts, integrations, `env.schema` | yes |
| `src/config/schema.ts` | The zod schema and the ten cross-field rules behind `pnpm check:config` | yes |
| `src/config/defaults.ts` | Every default the schema fills in | yes |
| `src/config/presets.ts` | Theme preset registry (`default`, `warm`, `mono`) plus each preset's OG hex colours | yes |
| `src/config/types.ts` | The public `SiteConfig` type | yes |
| `src/content/listings/` | One Markdown file per listing; the file name is the slug | yes |
| `src/content/blog/` | Blog posts | yes |
| `src/content/pseo/` | Optional per-slug intro/title overrides for generated pSEO pages | yes |
| `src/content/schema.ts` | Collection schemas: `listingSchema`, `blogSchema`, `pseoCopySchema` | yes |
| `src/content.config.ts` | Collection definitions (which loader feeds which schema) | yes |
| `src/loaders/files.ts` | The Markdown listings loader | yes |
| `src/loaders/normalize.ts` | Source → `Listing`: category check, tag filter, date coercion, `isFeatured` | yes |
| `src/pages/[...path].astro` | The only dynamic route; `getStaticPaths` builds every directory page | yes |
| `src/pages/` | Fixed pages: home, search, about, terms, privacy, 404, blog, `/rss.xml`, `/robots.txt`, OG endpoint | yes |
| `src/lib/routes.ts` | The pure route table (`buildRoutes`, matchers, reserved segments) | yes |
| `src/lib/paths.ts` | The only URL constructor in the project | yes |
| `src/lib/seo.ts` | Titles, descriptions, canonicals, `noindex`, sitemap chunks | yes |
| `src/templates/` | One template per page kind; the catch-all dispatches to these | yes |
| `src/components/listing/Detail.astro` | The listing detail body — the file to edit for "change the detail page" | yes |
| `src/components/` | Everything else presentational (`listing/`, `category/`, `blog/`, `pseo/`, `seo/`, `site/`, `ui/`, and `ads/` — the four placements `site.config.ts` `ads` switches on) | yes |
| `src/features/index.ts` | Facade for the paid-only features; templates import from here, never from `paid/` | replaced |
| `src/features/paid/pseo/patterns.ts` | The pSEO template registry — a new template's candidate logic goes here; `src/features/paid/pseo/generate.ts` handles `minItems`, collisions, `maxPages`, siblings and copy | deleted |
| `src/features/paid/og/` | Build-time OG image rendering (satori + resvg) | deleted |
| `src/styles/global.css` | Tailwind v4 entry: `@source` allow-list, dark variant, preset imports, `@theme inline` | yes |
| `src/styles/presets/` | One CSS file per theme preset | yes |
| `src/islands/SearchPanel.tsx` | The only hydrated component in the whole site | yes |
| `src/db/schema.ts` | The D1 schema. Migrations in `drizzle/` are generated from it | deleted |
| `src/loaders/d1.ts` | Build-time D1 REST loader (`LISTINGS_SOURCE=d1`) | deleted |
| `src/middleware.ts` | Guards `/admin/**` and `/_actions/`; lazy-imports the DB so prerender stays clean | deleted |
| `src/actions/` | The two Action namespaces: `submit` (public) and `admin` | deleted |
| `src/server/` | Everything that runs per request: `db`, `env` (the only reader of the astro env module), `token`, `admin-auth`, `submit`, `queries`, `meta-fetcher`, `media`, `jobs`, `queue`, `email/`, `rebuild/`, `auth/` | deleted |
| `src/pages/submit/` | The public two-step form and the token-addressed status page | deleted |
| `src/pages/account/` | Submitter accounts (`features.accounts`): `/account/` lists a signed-in submitter's submissions; `src/pages/login/` is the sign-in page and `src/pages/auth/` the emailed-link and Google callbacks. `src/server/auth/` holds sessions, links and the Google exchange | deleted |
| `src/pages/admin/` | Login, queue, listing detail, featured, stats | deleted |
| `src/worker.ts` | The Worker entry (`main`): the adapter's `fetch`, plus `scheduled` running the four hourly jobs | deleted |
| `integrations/sitemap.ts` | Chunked sitemap | yes |
| `integrations/pagefind.ts` | Search index, written at `astro:build:done` | yes |
| `integrations/budget.ts` | Static-file count against the Cloudflare plan limit | yes |
| `integrations/seo-report.ts` | Inbound-link / orphan / sitemap audit; can fail the build | deleted |
| `scripts/` | The CLI scripts behind the `pnpm` commands below | yes |
| `tests/unit/` | Vitest node tests (`vitest.config.ts`) | yes |
| `seed/datasets/ai-tools/` | The demo dataset `pnpm content:use` restores | yes |
| `public/` | Copied verbatim into the build: `public/favicon.svg`, `public/og-default.png`, `public/logos/`, `public/_headers` | yes |
| `.agents/skills/` | Task recipes; `pnpm skills:sync` copies them to `.claude/skills/` | deleted |
| `wrangler.jsonc` | Worker name, `main`, compatibility date, the `DB` / `MEDIA` / `EMAIL` bindings | replaced |
| `.env.example` | Build-time variables | yes |
| `.dev.vars.example` | Runtime secrets | deleted |

## Related

- [Commands](https://zerodirs.com/docs/start/commands/): every `pnpm` script, and which file it runs.
- [Configuration](https://zerodirs.com/docs/configure/site-config/): the one file in the tree a buyer edits first.
- [Free vs Pro](https://zerodirs.com/docs/about/free-vs-pro/): the line the last column draws, in prose.
