Project structure

Frontend — Astro

astro.config.mjs   site config and integrations
src/
  pages/           file-based routes; [...slug].astro renders each post
  layouts/         shared page shell
  components/      framework components, if you picked one at install time
  lib/             logger, HTTP client, and shared patterns
  content/         markdown, validated at build time
  content.config.ts  collection schemas
public/            copied to dist/ untouched

astro.config.mjs ships an .example sibling listing every first-level config key, commented out. It is never loaded — copy what you need into the real config.

Anything in src/components/ is an example, not a fixture: render one with a client directive (<VueCounter client:load />) where you actually want an island. Without a directive a framework component still renders, as static HTML with no JavaScript shipped.

Backend — Vite + Nitro

nitro.config.ts    server behaviour — routes, caching, presets
vite.config.ts     build pipeline; only plugin is nitro()
server/
  routes/          file-based routes; routes/api/* is the API
  utils/           helpers, exposed on `@/utils/*` and via `#imports`
  plugins/         runtime plugins — errors.ts wires request/response/error hooks

Each config ships a .example sibling listing every available key, commented out. Those files are never loaded; copy what you need into the real config.

Batteries that are already running

Three libraries ship wired in on both halves rather than merely installed, so there is working code to read (and delete) instead of a docs tab to open.

Consolasrc/lib/logger.ts is one tagged instance, used from .astro frontmatter (where it prints to the terminal at build time) and from a <script> (where it prints to the browser console). One logger, both places.

ofetchsrc/lib/api.ts is a preconfigured client: JSON in and out, a thrown FetchError (with the server’s parsed body on .data) instead of a response nobody checked, and retries with a timeout. Its base URL is configuration: set PUBLIC_API_BASE in .env.

magic-regexpsrc/lib/slug.ts builds a URL-slug pattern out of named parts rather than a literal. src/pages/blog/[...slug].astro checks every post id against it during getStaticPaths, so a filename that would produce an ugly URL is a warning at build time rather than a 404 later.

The server-side counterparts of all three live under server/utils/ — see API routes.

Content collections

src/content.config.ts defines each collection and its schema. Frontmatter is checked at build time, so a missing title or an over-long description fails the build naming the file and field, rather than turning up as undefined in a template.

Schemas import Zod from astro/zod — it ships inside Astro, so there is no separate zod dependency to add or keep in sync.

Partytown

@astrojs/partytown is pre-wired. Any script tagged type="text/partytown" runs in a web worker instead of competing with your own JavaScript for the main thread — worth doing for analytics and other third-party tags. src/layouts/Base.astro has a working example.