Spec 01 — Site Model
What a site is: pages, page types, blocks, menus, references, tokens, themes and derived data.
1. Site anatomy
A Quoin site is a Git repository with this normative shape:
content/
pages/
<page-type>/ # one directory per page type (collection)
*.md / *.yaml # one file per page
menus/ # menu trees, YAML
header.yaml
footer.yaml
site.yaml # site configuration (schema-validated)
templates/ # template registry (code)
blocks/ # block components (code)
styles/ # tokens + base styles (code)
trust.yaml # the trust dial (spec/04, schema-validated)
Additional directories (assets, CI workflows, tests) are permitted and
unregulated. The normative requirement is: all content lives under
content/ and site.yaml, and every file there is schema-validated.
2. Pages and page types
Every page is an entry in exactly one collection (§2.1) and is rendered by the template bound to that collection (§2.2).
2.1 Collections
A collection is defined by:
- a name (kebab-case, e.g.
posts,consultation-responses); - a schema (the Zod-style contract of
spec/02); - a path pattern for the published URL (e.g.
writing/{slug}/); - a template binding.
A site SHOULD use the reference library of five page types
(spec/02 §2) and MAY define additional types with their own schemas. Any
site-defined type MUST follow the same mechanics — frontmatter schema, body
Markdown, template binding — and MUST NOT bypass the chrome.
2.2 Templates
A template is a component that receives a validated page entry and renders it. Templates MUST NOT hard-code content and MUST NOT accept unvalidated data: if the template needs a field, the schema guarantees it.
A page type binds to one template. A template MAY be parameterised by a
variant field (a token, §6) so one collection can present differently —
e.g. a posts collection whose entries declare variant: ride | book-note | news.
2.3 Composed pages
A composed page is a page whose body is an ordered list of blocks:
blocks:
- type: hero
data: { heading: "…", images: [ … ] }
- type: card-grid
data: { cards: [ … ] }
Every block type has its own schema (spec/02 §3); the composed-page schema
validates the list (type known, data conforms). Blocks are the compositional
vocabulary of the site: the same block library renders the hero of a charity
home page and the stats panel of a personal site.
2.4 The blob
A blob is a page of type blob: freeform Markdown/MDX rendered inside the
chrome. It exists for genuinely unique pages that the block library cannot
express.
Rules (Constitution §7):
- A blob MUST NOT replace, hide, or reimplement any part of the chrome.
- A blob SHOULD NOT be the first response to a new requirement; if the need recurs or is expressible as blocks, the block SHOULD be added to the library instead.
- Every blob is recorded in the site’s notes with a one-line justification.
Why: the fewer custom pages, the more flexible the design. Each blob is a piece of the site the shared model cannot reason about; the audit keeps them visible and rare.
3. Menus
Menus are named, ordered trees stored as YAML under content/menus/:
items:
- label: Cycling
ref: posts/cycling-index
- label: Croftsware
href: https://www.croftsware.com
- An item carries
ref(a page reference — preferred) orhref(any URL: internal path, same-page anchor/#about, or external). - Items may nest (dropdown children).
- A site defines any number of named menus; the chrome renders them where configured (header, footer, banner subnav). Multiple menus per site are normal and expected.
4. Site configuration
site.yaml is validated by schema and carries the site’s identity: name,
domain, contact email, optional registration fields (charity number, company
number), footer links, social links, and chrome configuration (which menu is
the header, which is the footer). Fields are optional but, when present,
validated — the chrome can rely on a configured charity number being a
well-formed registration number.
5. References between content
Page entries may reference other entries by (collection, id) (the Astro
content reference() pattern). This is how a composed home page features the
latest document, or a card grid links to service pages. References are
resolved at build; a reference to a missing entry fails the build.
5.1 Featured refs
featured/<slot> is a valid reference wherever a (collection, id) ref is
legal — block data, menu items, CTAs. It resolves at build to the single
entry carrying featured: <slot> in its frontmatter (spec/02 §9).
Resolution failure — no carrier, or more than one — fails the build exactly
as a dangling ref does.
Why: “the featured X” is one fact. When it is authored separately into every surface that shows it (hero, feature block, menus), the copies drift — found live on the reference site, where four authored copies of “latest submission” sat nine months stale. Surfaces that share a slot cannot disagree; the answer is computed once.
6. Styles and tokens
The style layer owns design tokens (colours, spacing, type scale) and block
variants. Content MAY reference tokens by name (e.g. variant: dark); the
schema validates the name against the registry. Content MUST NOT carry raw
CSS, inline styles, or presentation markup beyond what Markdown provides.
Why: “reusable styles” is a frontend concern. The CMS-side of it is only ever a validated variant name.
7. Derived data (projections)
Aggregates, counts, index pages, recent lists, and sitemaps MUST be computed
at build from the collections (Constitution §8). A projection is a build-time
function: collections in, data out. It MUST NOT be authored content, and a
schema MUST reject a field whose name and shape indicate hand-maintained
derived data (e.g. totalRides: 209 in frontmatter).
8. Slugs and URLs
Each page has a stable slug; the collection’s path pattern turns it into the
published URL. Changing a slug is a structural change (highest change class,
spec/04): it breaks inbound links, and old URLs SHOULD be redirected at the
CDN layer.
9. Themes and the theme contract
Content, structure, and theme are independently changeable axes. The theme
axis lives in one or more theme packages: themes/<name>/ directories,
each providing the chrome, a renderer per block type it supports, a template
per page type, styles, and a manifest (theme.yaml: name, blocks,
pageTypes, tokens — the normative schema is in spec/02 §8).
The site selects exactly one active theme in site.yaml (theme:). The
division is enforced at build:
- The active theme MUST exist and carry a valid manifest; otherwise the build fails.
- Every block type used by content MUST be declared in the active theme’s manifest. A build that cannot render declared structure fails loudly — never a silent visual break.
- The manifest’s
tokenslist defines which design knobs the theme consumes; token writes (spec/04tooling) accept only manifest-declared names, and a theme consuming a token absent fromtokens.yamlfails the build. - Changing the active theme MUST leave content and structure byte-identical — a theme swap is one config line plus a rebuild.
- Adding a block type is a structural change that MUST update the schema and the manifest/renderers of every theme serving content that uses it. The manifest check makes this coupling loud and conscious rather than accidental.
Themes MAY compose (a theme may reuse another theme’s components) but each theme’s manifest MUST be complete for the content it serves.