Accepted
ADR 0025 — Canonical example site
One in-universe example site, bundled into the Worker and kept in lockstep with the code
Decision
Cusp ships one deliberate example site: The Royal Society of Pheasant Patissiers, a learned Victorian society documenting Britain's pancake-making pheasants. It is screenshot-memorable, it has a natural reason to keep producing content — a quarterly periodical never runs out of dispatches — and it gives time travel a real in-universe excuse, because a pheasant's signature recipe legitimately evolves over the years.
One example, not a menu of three. A menu of three is a menu of none: each rots at a different rate and none gets the maintenance that lockstep affords. Investing in a single example is the cheaper path to a credible demo.
Three forces, one artefact
- Discovery — someone cloning the repo wants to see Cusp doing what Cusp does, not a blank admin shell.
- Demonstration — the product story needs a concrete, pokeable example of what an editor-first CMS feels like.
- Living integration test — features that ship without being wired into a real-shaped site decay silently. Making the example a first-class artefact, exercised by CI, prevents that.
Fixtures as JSON
The fixtures live as JSON files under examples/pheasant-patissiers — a manifest, settings, menus, and one file per collection, field-definition set, and document. JSON because it reviews as content rather than TypeScript noise, it is hand-authorable by contributors writing recipe copy, and it is the shape a future admin export of demo content would produce. Every fixture is validated against a schema with a default-deny posture: unknown keys reject, missing required keys reject.
A document field that references another document cannot carry a live id, because ids are minted at seed time. Fixtures use a $ref sentinel naming the target collection and slug, and the seed resolves it to the live id before each write — including inside version-history entries. The build-time embed enforces that a reference target seeds before its referrer.
Bundled and seeded
The Worker cannot read files at runtime, so a build-time embed script validates every fixture and compiles the bundle into a generated TypeScript module — the same pattern Cusp uses to embed SQL migrations. The validation in that script is the first line of defence: a malformed fixture fails the build, not the runtime install.
The SeedExampleSite workflow, bound as SEED, runs the seed end to end through idempotent, purely additive steps: upsert collections, field definitions, and documents; publish or schedule documents; and apply settings and menus. Documents that carry a version history get one create and several updates so the version chain accumulates — that is what makes the time-travel demo real. The seed never deletes; re-running it adds new versions of the seeded entities rather than overwriting them, and it leaves site-identity settings the wizard already captured (title, locale, public base URL) untouched.
Two triggers, one workflow
The first-run wizard offers a single checkbox to seed sample content, off by default. The contributor CLI (pnpm seed:example) triggers the same workflow against a running dev server using an admin session cookie. Both paths run the identical workflow.
The lockstep rule
This is the load-bearing convention. Whenever a feature, field type, block type, workflow, binding, route, setting, or admin behaviour changes, the same commit updates the example site so it exercises the new capability. A change that ships a feature without exercising it in the example is incomplete, exactly like a change that ships without doc updates.
Two layers enforce it. The process layer is reviewer and contributor discipline. The tooling layer is an integration test that runs the seed against a fresh database and asserts the collections, documents, version chains, and scheduled dates all hold. A change that breaks the seed is red in CI.
Consequences
- New users see Cusp working within seconds of seeding; contributors have a real-shaped site to test against; every release ends with a working demo because lockstep and CI keep it current.
- Every feature change carries a small extra obligation to update the example — sometimes one line, sometimes more.
- Bundled fixtures count toward the Worker bundle budget and are tracked as a line item; if they ever grow too large the fallback is fetching them from R2 at install time.
- Considered and rejected: a separate demo repo (decays faster than the code, doubles the wiring, taxes new users), better screenshots (stale, not pokeable, not testable), and reusing test fixtures (tests optimise for the smallest proof, a demo for a real-looking site — forcing one to serve both makes both worse).