AI agent index: llms.txt — generated from AGENTS.md and .claude/ by pnpm gen:llms-txt, served at /llms.txt
A production-ready template for building content-first sites and SSR apps on Cloudflare Workers with Astro. Ships with Tailwind CSS v4, native Astro API endpoints, a strict Biome + Vitest toolchain, an optional Drizzle + Neon data layer, and the full Auditmos baseline (simple-git-hooks, knip, taze, semantic-release, .claude/ rules).
Use it as the starting point for your next project — clone it, rename it, decide whether you want the data layer, and start shipping.
- Click Use this template on GitHub (or
gh repo create --template). pnpm install.pnpm run init-project— prompts for a kebab-case project name, renameswrangler.jsonc+package.json, and fans out.env.example→.envand.dev.vars{,.staging,.production}.example→.dev.vars/.dev.vars.staging/.dev.vars.production. Pass--with-dbto add the Drizzle/Neon data layer scaffolding. Idempotent — re-runnable, never overwrites filled-in files.- (Only if
--with-db) Provision a Neon database and fillDATABASE_HOST/USERNAME/PASSWORDin.dev.vars(and the staging/production variants when you deploy them). - Run
pnpm cf-typegen && pnpm dev.
See Quick Start below for the dev-loop commands.
- Edge-first — Astro
output: "server"running on Cloudflare Workers via@astrojs/cloudflare. Bindings typed automatically throughworker-configuration.d.ts. - No framework lock-in — pure Astro by default. Add React/Vue/Svelte islands per project with
pnpm astro add <integration>only when you actually need them. - Type-safe end-to-end —
astro check+ strict TypeScript, Zod at every endpoint boundary, typed CloudflareEnv. - Three-env wrangler —
dev,staging,productionblocks shipped out of the box, one Worker each. Deploy withpnpm deploy:staging/pnpm deploy:production. - Agent-friendly — project rules in
.claude/rules/activate automatically based on the files you touch. Mirrors the conventions of every other Auditmos template repo (saas-on-cf,tstack-on-cf,hono-on-cf).
# Install dependencies
pnpm install
# Generate Cloudflare Env types
pnpm cf-typegen
# Start the dev server
pnpm devThe app runs on http://localhost:3000.
| Script | Purpose |
|---|---|
pnpm dev |
Dev server on port 3000 (Astro + Vite) |
pnpm build |
Production build to ./dist |
pnpm preview |
Preview the production build locally |
pnpm deploy:dev / pnpm deploy:staging / pnpm deploy:production |
Build and deploy to the named Cloudflare environment |
pnpm cf-typegen |
Generate Env types from wrangler.jsonc |
pnpm test / pnpm test:watch / pnpm test:coverage |
Vitest |
pnpm types |
astro check + tsc --noEmit |
pnpm lint / pnpm lint:fix |
Biome check / auto-fix |
pnpm knip |
Detect unused files, deps, and exports |
pnpm deps / pnpm deps:update |
Check / apply dependency updates via taze |
pnpm deps:major / pnpm deps:major:update |
Show / apply major updates — a decision, never automated |
pnpm deps:major:report |
Report pending majors into the tracking issue (--dry-run prints it instead) |
pnpm release |
semantic-release (CI only) |
If you opted into the data layer with --with-db, you also get db:generate:{dev,staging,production}, db:migrate:*, db:pull:*, db:studio, and db:seed:* — all wired through @dotenvx/dotenvx.
src/
├── pages/ # File-based routes
│ ├── index.astro # Landing page
│ ├── sitemap.xml.ts # Sitemap route (SSR — nothing here is prerendered)
│ ├── robots.txt.ts # Robots route, pointing at the sitemap
│ └── api/ # API endpoints (./api/<name>.ts)
│ └── health.ts # Exemplar: parse, delegate, respond
├── health/ # The pure module it delegates to, plus its tests
├── seo/ # Sitemap, robots and link-preview tags
├── middleware.ts # Renders, then applies the security-header baseline
├── security-headers/ # That baseline — the only place a header is named
├── layouts/
│ └── Layout.astro # Shared HTML shell
├── styles/
│ └── globals.css # Tailwind v4 entry
└── env.d.ts # App.Locals typed against CF Env
Path alias @/* resolves to src/*.
Reusable components get their own folder under src/ when a second page needs one,
and a domain module gets src/<domain>/ when the domain appears — following
src/health/. The template ships no empty directories waiting to be filled.
docs/ holds business requirements and design docs; plans/ holds phased
implementation plans.
wrangler.jsonc is the source of truth — read the values there,
not here. This README used to reproduce the file inline, which meant the weekly
dependency bot bumped the real compatibility date and left the copy a little more
wrong every time. The copy is gone rather than synced: teaching the bot to rewrite
prose automates a problem better deleted. scripts/docs-truth.test.ts fails if a
copy is reintroduced.
What the file declares:
- Worker name, entrypoint, compatibility date and flags (
nodejs_compat) assets—./distserved through theASSETSbindingobservabilitywith an explicit head sampling rate, plus source map upload- Three
envblocks —dev,staging,production— one Worker each - Commented example blocks for the bindings you are most likely to add next
Conventions:
- Use
wrangler.jsonc, not.toml. - Prefer
custom_domain: trueover routes withzone_name— see.claude/rules/cloudflare-deployment.md. - Run
pnpm cf-typegenwhenever you add bindings to regenerateworker-configuration.d.ts.
// src/pages/api/hello.ts
import type { APIRoute } from "astro";
import { env } from "cloudflare:workers";
export const GET: APIRoute = () => Response.json({ env: env.CLOUDFLARE_ENV });In Astro v6 + @astrojs/cloudflare v13 the Astro.locals.runtime proxy is gone — import { env } from "cloudflare:workers" is the only supported path for bindings and vars.
Per-environment vars live in .dev.vars / .dev.vars.staging / .dev.vars.production, never committed. This matches the wrangler convention — .dev.vars.<env> is loaded ahead of .dev.vars when CLOUDFLARE_ENV=<env> is set (docs). For staging/production, mirror the same keys as Cloudflare secrets via wrangler secret put --env <name>.
astro.config.mjs reads the site origin from SITE_URL, falling back to a
placeholder. It is a build-time value — not a Worker binding — so it is set on
the build, and every deploy script builds first:
SITE_URL=https://staging.example.com pnpm deploy:stagingThat origin is what canonical URLs, /sitemap.xml and /robots.txt resolve
against. Left unset, all three fall back to the origin the request arrived on,
which is right often enough to be safe and never as good as configuring it.
Both files are routes rather than static assets (src/pages/sitemap.xml.ts,
src/pages/robots.txt.ts) — this template renders on
demand and prerenders nothing, so a build-time sitemap integration would have no
routes to enumerate. Link-preview tags come from the same module,
src/seo/, driven by the layout's title and description props.
pnpm deploy:dev # build + wrangler deploy --env dev
pnpm deploy:staging # build + wrangler deploy --env staging
pnpm deploy:production # build + wrangler deploy --env productionEvery deploy command names its environment, and there is deliberately no environment-less one. The old bare script was unreachable — deploy is a pnpm built-in (workspace deploy), so the package manager answered instead of the script — and the wrangler invocation it wrapped published to the top-level Worker, which none of the three env blocks owns. That is a fourth live Worker nobody manages. scripts/deploy-scripts.test.ts fails if such a script is reintroduced.
Deploys are manual by design. CI gates lint, types, tests, dead code and the production build, but never ships — a template its consumers clone must not demand a CLOUDFLARE_API_TOKEN they have not created. To automate it for your own project, add a workflow step running one of the commands above with CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in the job environment; see Wrangler CI/CD.
Re-run pnpm run init-project --with-db (or pass the flag the first time) to scaffold:
drizzle-{dev,staging,production}.config.tssrc/db/{client,health,schema.ts,setup.ts}db:*scripts wired through@dotenvx/dotenvxDATABASE_HOST/USERNAME/PASSWORDkeys in every.dev.vars{,.staging,.production}.example
Same conventions as tstack-on-cf and hono-on-cf: domain-per-folder, narrow public API, per-env migration directories.
This template is set up for agent-assisted development:
CLAUDE.md→ symlink toAGENTS.md— project-wide agent guide..claude/rules/— topic rules (general.md,deep-modules.md,error-handling.md,atomic-imports.md,cloudflare-deployment.md, plusfrontend/{astro,tailwind-v4}.mdandapi/{cloudflare-workers,astro-endpoints}.md) that activate automatically based on the files being edited..claude/agents/—dd-w(design-doc writer),dd-i(design-doc implementer),mvp-e(MVP enforcer)./docs— single source of truth for business requirements / design docs.llms.txt— the agent index, generated from the two above bypnpm gen:llms-txt.public/llms.txtsymlinks to it so a deployed clone serves it at/llms.txt; there is no second copy to keep in sync. A drift test fails if the committed file falls behind the tree.
- Astro — content-first framework with islands architecture
- @astrojs/cloudflare — Cloudflare Workers adapter
- Tailwind CSS v4 — utility-first CSS, configured via CSS
- Cloudflare Workers — edge computing platform
- Biome — fast formatter and linter
- Drizzle ORM (when
--with-db) — type-safe SQL - Neon (when
--with-db) — serverless Postgres
Open source under the ISC License.