# Make it yours

> The first hour after pnpm dev works — name the site, set its URLs, categories and tiers, replace the demo content, brand it, switch features on, and build.

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/make-it-yours/

---

Everything here is an edit to `site.config.ts` or one command, in the order that avoids redoing
work: the noun and the URL scheme before the content, `site.url` before the build. Each step ends
with a check; the [Quickstart](https://zerodirs.com/docs/start/quickstart/) and `pnpm dev` come first.

## Setup

1. Name it.

   ```ts title="site.config.ts"
   site: {
     name: 'Acme Directory',                  // header, footer, page titles, JSON-LD
     tagline: 'The best widgets, ranked',     // the home page heading and its title
     description: 'A curated directory of widgets, compared on price and features.', // home meta description
     url: 'https://acme.com',                 // https, no trailing slash
     contactEmail: 'hello@acme.com',          // footer and about page
     locale: 'en',                            // html lang, feed language, date formatting
     kind: 'software',                        // software | service | business
   },
   ```

   `description` is capped at 160 characters: it is the home page's meta description. `kind`
   sets the default of `seo.jsonLd.listing`: `software` emits SoftwareApplication, `service`
   Organization, `business` LocalBusiness — which needs `address` and `telephone` custom fields
   on the listing, else Organization.

   `pnpm check:config` answers `site.config.ts OK — Acme Directory (https://acme.com); …`.

2. Give it its URLs and its noun.

   ```ts title="site.config.ts"
   routes: {
     listingBase: 'widgets',        // /widgets/<slug>/ and /widgets/page/2/
     categoryBase: 'categories',    // /categories/<slug>/
     tagBase: 'tags',               // /tags/<slug>/
     listingNoun: { singular: 'widget', plural: 'widgets' },
   },
   ```

   The three bases must differ, and none may be a reserved segment such as `blog`, `search` or
   `admin`: `site.config.ts › routes.listingBase: "blog" is a reserved path segment`. `listingNoun` fills
   `{noun}` and `{nouns}` in every SEO template, the footer's **Submit a widget** link and the
   about page. Decide the bases now: changing one later changes every canonical, sitemap entry
   and OG image URL, and the old pages stop existing.

   Open `http://localhost:4321/widgets/`: the listing index, paginated at `listing.perPage`.

3. Categories and tags.

   ```ts title="site.config.ts"
   categories: [
     { slug: 'blue', name: 'Blue', description: 'Widgets that are blue.', icon: 'palette', order: 1 },
     { slug: 'fast', name: 'Fast', description: 'Widgets that are quick.', icon: 'zap', order: 2 },
   ],
   tags: [
     { slug: 'open-source', name: 'Open source', description: 'Source available.' },
   ],
   ```

   A category is a page, a breadcrumb and a sitemap entry; a tag is a label. `order` sorts the
   chips, the grid and the neighbour links; `seoTitle` and `seoDescription` replace the templates
   for that page. `icon` is a lucide icon name (browse lucide.dev); `defineSiteConfig()` merges
   category icons into `theme.icons`, the list astro-icon bundles, so never add them there — that
   list is for template icons; `src/icons/` holds your own SVGs.

   Every listing's `category` must be one of these slugs, or the build stops and the previous
   deploy stays live:

   ```text
   listings › my-widget: unknown category "green" (slug "my-widget"); categories in site.config.ts: blue, fast
   ```

   An unknown tag is only dropped:
   `listings › my-widget: unknown tag(s) "beta" filtered (slug "my-widget"; not in site.config.ts tags)`.
   An unused tag gets no page; a category always does, `noindex` below `seo.noindexBelow`
   listings.

   `pnpm check:config` counts `<n> categories, <n> tags`; on `/widgets/` the chips are now yours:

   ![Listing index: category chips and listing cards](../../../assets/screenshots/listing-index.png)

4. Tiers.

   ```ts title="site.config.ts"
   tiers: [
     { id: 'free', name: 'Free', description: 'Reviewed in order, listed for free.', kind: 'queue', priceCents: 0, currency: 'usd', enabled: true },
   ],
   ```

   Required in files mode too (`at least one tier is required`, one enabled): every listing's
   `tier` reads it, and the free tier alone is complete. A priced tier (`kind: 'express'`, or
   `'featured'` with `featuredDays`) can only be enabled with `features.payments: true` and
   `providers.payment: 'stripe'`; the demo ships both priced tiers on, so turning payments off
   without disabling them fails: `tiers[1].priceCents: must be 0 when features.payments is false`.
   Keep the prices; the pricing table renders from them. Selling is
   [Plans and payments](https://zerodirs.com/docs/submissions/payments/).

   `pnpm check:config` prints `<n> tiers (<n> enabled)`.

5. Validate.

   ```sh title="Terminal"
   pnpm check:config
   ```

   A failure names every field path at once:

   ```text
   site.config.ts is invalid (2 issues):
   site.config.ts › site.description: must be at most 160 characters (home meta description)
   site.config.ts › routes.tagBase: must differ from routes.categoryBase ("categories")
   ```

   Success prints the counts and the file estimate against `budget.plan`:

   ```text
   site.config.ts OK — Acme Directory (https://acme.com); <n> categories, <n> tags, <n> tiers (<n> enabled), <n> pSEO templates, preset "default", <n> icons
   listings: <n> (src/content/listings); logos served directly
   budget: <n> listings × <n> files + 3000 overhead ≈ <n> / 20000 files (free plan, <n>%) — ok
   headroom: up to ~<n> listings before the 90% limit on the free plan
   ```

   Above 80% of the plan it warns, above 90% it exits 1, as the build would —
   [Cost and limits](https://zerodirs.com/docs/run/cost-and-limits/). Every field is in the [reference](https://zerodirs.com/docs/configure/reference/).

6. Replace the demo content.

   ```sh title="Terminal"
   pnpm content:reset --yes
   ```

   Without `--yes` it only prints what it would delete. With it, it empties
   `src/content/listings/`, `src/content/blog/` and `public/logos/`, keeps `unpublished-draft.md`
   (the `draft: true` fixture), and writes one `example-<category>.md` per category from your own
   config — first two tags, first pricing type, first tier, no logo — capped at 6 (`--count N`),
   plus a `hello-world.md` post:

   ```text
   reset: removed <n> listings, <n> blog posts, <n> logos
   reset: wrote <n> example listings (one per category, capped at 6 of <n> categories) and src/content/blog/hello-world.md
   reset: kept src/content/pseo/example-override.md
   reset: kept src/content/blog/unpublished-draft.md (starter fixture, draft: true — delete it if you do not want it)
   next: pnpm check:config && pnpm dev  —  or tsx scripts/content.ts use ai-tools --yes to put the demo data back
   ```

   Then bring your listings in:

   ```sh title="Terminal"
   pnpm listings:import widgets.csv --dry-run   # every bad row in one pass; writes nothing
   pnpm listings:import widgets.csv
   ```

   CSV or JSON, columns on [Listings and datasets](https://zerodirs.com/docs/configure/data/); an unknown category is a
   hard error here too. For a different sample, `pnpm content:use indie-tools --yes` copies that
   dataset in and prints the `categories` and `tags` blocks to paste, never editing
   `site.config.ts`.

> **The demo content is not yours to publish**
>
>    The AI-tool entries and demo blog posts describe real products and are there to show you a full
>    site, not to be republished as your own editorial. The SVGs in `public/logos/` are generic
>    initial-letter tiles: keep, replace or delete them.

   `pnpm check:config` now says `listings: <n> (src/content/listings)`, and every category page
   shows its example.

7. Brand it.

   Four pieces of ZeroDirs branding live outside the config:

   | What | Where | What to do |
   | --- | --- | --- |
   | Favicon and header logo | `public/favicon.svg` | Overwrite it: `site.logo: '/favicon.svg'` makes it the header logo and the JSON-LD Organization logo. Remove `site.logo` for a text-only header |
   | Social-share image | `public/og-default.png` | `pnpm og:default` regenerates it from `site.name`, `site.tagline` and `site.description`. Every page without a generated image uses it; under `seo.ogImages: 'static'`, every page |
   | About page | `src/pages/about.astro` | The paragraphs under "How widgets get in" and "Featured placement is disclosed" are the demo's editorial policy — rewrite them |
   | Handles | `seo.twitterHandle`, `site.social` | the `twitter:site` meta tag; the footer's X and GitHub links and `sameAs` in JSON-LD. All still say `zerodirs` |

   The footer is all config: name, tagline, contact email, the nav, the links each feature
   switches on, **Terms** and **Privacy** once `legal.termsUrl` and `legal.privacyUrl` are set
   (both pages ship), and **Built with ZeroDirs**, off with `site.builtWith: false`; the licence
   does not require it.

   ![The demo footer: link columns, newsletter form, Terms, Privacy and Built with ZeroDirs](../../../assets/screenshots/footer.png)

   `pnpm og:default --check` answers `public/og-default.png is up to date (<n> bytes, "Acme Directory")`
   and exits 1 when stale — put it in CI.

8. Switch things on or off.

   The defaults need no account; the demo file turns on what its account provides. A flag without
   the thing it needs buys a route that answers 503 — open its page first.

   | `features.` | Default | Demo | What it adds | Page |
   | --- | --- | --- | --- | --- |
   | `submissions` | `false` | `true` | the Submit link; the form and review queue need D1 | [Submissions](https://zerodirs.com/docs/submissions/form/) |
   | `accounts` | `false` | `true` | sign-in by emailed link or Google, and `/account/` | [Submitter accounts](https://zerodirs.com/docs/submissions/accounts/) |
   | `payments` | `false` | `true` | Stripe Checkout for priced tiers; needs two secrets | [Plans and payments](https://zerodirs.com/docs/submissions/payments/) |
   | `newsletter` | `'off'` | `'d1'` | the footer form: `'embed'` renders `newsletter.embedHtml`, `'d1'` stores subscribers in D1 | [Newsletter](https://zerodirs.com/docs/grow/newsletter/) |
   | `blog` | `true` | `true` | `/blog/`, `/blog/rss.xml`, the Blog nav item | [Blog and feeds](https://zerodirs.com/docs/configure/blog-and-feeds/) |
   | `search` | `true` | `true` | `/search/` and the search boxes; the index exists only after `pnpm build` | [Search](https://zerodirs.com/docs/configure/search/) |
   | `clicks` | `'off'` | `'ping'` | click counting through `/out/<id>/`: `'ping'` adds a beacon, `'redirect'` routes the click | [Click tracking](https://zerodirs.com/docs/grow/clicks/) |
   | `relatedLinks` | `true` | `true` | the related list on detail pages | [Programmatic SEO](https://zerodirs.com/docs/configure/pseo/) |
   | `pseo` | `true` | `true` | the generated pages from `pseo.templates` | [Programmatic SEO](https://zerodirs.com/docs/configure/pseo/) |
   | `honeypot` | `true` | `true` | a hidden field on both forms; a filled one is dropped silently | [Submissions](https://zerodirs.com/docs/submissions/form/) |

   Ads are not a flag: each placement is a boolean in the `ads` block —
   [Ads and sponsors](https://zerodirs.com/docs/grow/ads/). The demo ships its top banner as a house ad for ZeroDirs;
   give `ads.topBanner` your own text and `url`, or set it to `enabled: false`.

   `pnpm check:config` fails when a flag contradicts another block, and `/admin/` lists what a
   switched-on flag still needs, such as
   `Missing secret STRIPE_SECRET_KEY — run: wrangler secret put STRIPE_SECRET_KEY (local: add to .dev.vars). Paid tiers cannot complete a checkout.`

9. Pick a theme.

   `theme: { preset: 'warm' }` — the presets are `default`, `warm` and `mono`; `darkMode` is
   `system`, `light` or `dark`; `theme.tokens` overrides colours, radii and fonts —
   [Theming](https://zerodirs.com/docs/configure/theming/). `pnpm check:config` prints `preset "warm"`.

10. Set `site.url` to the real domain, and build.

    ```sh title="Terminal"
    pnpm check:config && pnpm build
    ```

    `site.url` is baked in at build time: canonicals, the sitemap, both feeds and the absolute
    `og:image` URLs embed it. Built with the demo value, every canonical points at someone else's
    domain, and search engines follow it; `pnpm cf:setup` refuses to run while it still says
    `demo.zerodirs.com` or `example.com`. Read these lines:

    ```text
    routes: <n> directory pages (<n> listings, <n> index pages, <n> category pages, <n> tag pages, <n> pseo, <n> noindex) → node_modules/.cache/zerodirs/routes.json (<n> routes incl. static pages)
    pseo: generated <n> (dropped by minItems <n>, truncated <n>)
    sitemap: <n> urls in <n> chunks (listings=<n>, taxonomy=<n>, pseo=<n>, blog=<n>, pages=<n>); dropped <n> (search/noindex/disallow/file); pseo slugs: <n>
    budget: <n> files / 20000 (<n>%) — free plan, ok
    zero-js: <n> content pages, 0 islands, 0 external scripts — ok
    seo-report: scanned <n> pages; <n> listings; <n> noindex; pseo <n> pages (best-category-for-tag=<n>, name-alternatives=<n>)
    seo-report: inbound links per listing: min <n>; orphans (fewer than 2 inbound): 0 [strictLinks=on]
    ```

    `budget` and `zero-js` must end in `ok`. With `seo.strictLinks: true`, the demo's setting, an
    orphan listing fails the build; the default `false` only warns.
    [SEO](https://zerodirs.com/docs/configure/seo/#the-build-report) explains each line.

## Verify

`pnpm preview` serves the build. Open these on the URL it prints:

| | Open | You should see |
| --- | --- | --- |
| The home page | `/` | your name in the header, your tagline as the heading, your categories as chips |
| A category | `/categories/<slug>/` | its listings — after the reset, the one example — and its icon beside the heading |
| A listing | `/widgets/<slug>/` | the detail page, an initial-letter tile until it has a `logo` |
| Robots | `/robots.txt` | `User-agent: *`, `Disallow:` lines, `Sitemap: https://acme.com/sitemap-index.xml` with your domain |
| The sitemap | `/sitemap-index.xml` | `sitemap-listings-0.xml`, `sitemap-taxonomy-0.xml`, `sitemap-blog-0.xml`, `sitemap-pages-0.xml` — and `sitemap-pseo-0.xml` once a generated page exists — each starting with your `site.url` |

## Next

- [Go live](https://zerodirs.com/docs/deploy/go-live/) — One command to your own domain, then the short list of things only you can do.
- [Listings and datasets](https://zerodirs.com/docs/configure/data/) — The Markdown contract, the importer, the sample datasets, and moving to a database.
- [Theming](https://zerodirs.com/docs/configure/theming/) — The presets, the tokens you can override, dark mode and fonts.
- [Working with an agent](https://zerodirs.com/docs/agents/overview/) — Read this before the first prompt if you plan to hand it to Claude Code or Cursor.
