Commands
| Command | Action |
|---|---|
pnpm install |
Install dependencies |
pnpm dev |
Dev server at localhost:4321 |
pnpm build |
Production build to dist/ |
pnpm preview |
Serve the production build |
pnpm check |
Type-check .astro and .ts (astro check) |
pnpm lint |
ESLint |
pnpm format |
eslint --fix, then Prettier |
pnpm test |
Every *.test.ts self-check under src/ |
pnpm test:cms |
The CMS’s Vitest suite, under happy-dom |
pnpm wiki:lint |
Verify the repo wiki’s citations and links |
pnpm astro … |
The Astro CLI |
The chain
pnpm lint && pnpm check && pnpm build && pnpm test && pnpm wiki:lint
That is the full verification chain and the one to run before every commit.
What each one covers
pnpm build is the real check. Content-schema mistakes, config mistakes and cross-file drift all surface here rather than at runtime, because the Zod schemas and the config parity tests run at build time. It also enforces the SITE_URL gate — a production build refuses to ship the example.com placeholder — and adminPackage refuses to build at all under output: "static".
A build emits dist/client/ (31 prerendered pages plus assets) and dist/server/ (the node entry that serves /contact/ and the admin).
pnpm test runs every *.test.ts under src/ — 135 of them — through Node’s --experimental-strip-types. No framework, no fixtures, no config. The discovery runner is one file in scripts/, and it fails if it finds none, so a check cannot go missing unnoticed.
That constraint is why the tested helpers import nothing from astro:* and use relative, extension-bearing specifiers: plain Node resolves neither a tsconfig path alias nor an extensionless TypeScript import. src/js/contact.ts, src/js/blog.ts, src/js/projects.ts and src/js/schema.ts are all written that way on purpose.
The checks worth knowing about, because they catch things a type cannot: the config parity tests (navData, footerData, servicesData — three pairs of lists stating the same fact twice), galleryModes.test.ts (which reads the stylesheet and fails if a gallery mode has no hide-rule), sectionImage.test.ts (which fails if the image library path and the section glob drift), tokens.test.ts, primitiveDrift.test.ts (which counts what has diverged between the two primitive copies), and boundary.test.ts (which asserts the four host files the CMS mount costs and prints the list on every run).
pnpm test:cms is separate because the admin’s specs need a DOM and a synced content store. It runs astro sync first, then Vitest under happy-dom. pnpm test deliberately needs neither, which is what keeps it framework-free.
If the CMS suite reports collections as empty, the cause is almost always cacheDir: Astro writes the content store to .astro/ in dev and to cacheDir elsewhere, so without the explicit cacheDir: "./.astro/" in astro.config.mjs, astro sync and Vitest fill and read different files with no error pointing at it.
pnpm check is astro check — type-checks .astro and .ts together. This is what catches an illegal <Icon name="…">, a SiteHref that is not trailing-slashed, and a config object that no longer satisfies its type.
pnpm lint is ESLint with the Astro, JSX-a11y and simple-import-sort plugins. pnpm format runs eslint --fix and then Prettier with the Astro and Tailwind plugins, so class order is normalized.
pnpm wiki:lint does for the repo’s wiki/ what the tests do for the code: it resolves every path:line citation and checks the cited line still contains the symbol the prose names. It exists because a 2026 audit found seventeen silently-drifted citations that no bounds check or date check could see.
The dev server
pnpm dev
Comes up on http://localhost:4321 with every route available, admin included. Two things behave differently there than you might expect.
A page added under src/admin/pages/ while the dev server is running answers 404 until you restart — the admin’s route table is read once at config load rather than re-scanned per request. Editing an existing admin page still hot-reloads normally. Your own src/pages/ is unaffected; that is Astro’s own file router.
And /examples/ui exists in dev but emits no paths in a production build, so you cannot preview it from pnpm preview.
Running the built server
pnpm build
node ./dist/server/entry.mjs
That is what the container runs, and what pnpm preview runs under the hood. It reads HOST and PORT.
If the CMS is mounted, that process is also the admin, and it expects to be sitting in a git checkout with node_modules present — see Deployment.
Commands the CMS runs
The publish build gate runs whatever you set as the build command on the Settings screen, through sh -c in the checkout. It is a shell sentence, not an argv, so pnpm i --frozen-lockfile && pnpm build works. Commands and checks covers the CMS side.