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.
Consola — src/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.
ofetch — src/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-regexp — src/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.
