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) or href (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.

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 tokens list defines which design knobs the theme consumes; token writes (spec/04 tooling) accept only manifest-declared names, and a theme consuming a token absent from tokens.yaml fails 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.