ADR-0008: Content models are Wagtail snippets, not Pages¶
Status¶
Accepted — implemented alongside ADR-0006 decision 1.
Date¶
2026-07-09
Context¶
PR #10 modelled content (Passage, Book, Letter, Essay) as Wagtail Page subclasses under
a structural page tree (SiteRoot → per-type index pages), plus a HomePage placeholder. That
bought Wagtail's publish workflow, but editors got the page-explorer navigation: content nested
inside a tree, reached by drilling through container pages.
The editorial team's CMS mockup wants flat, admin-style listings — grouped sidebar sections
(evergreen content / editorial / curation / taxonomy / media) with per-type tables (Title / Kind /
Status) — not a tree. And this service is a headless CMS for a mobile app: no content is ever
served at a URL by Wagtail's page-routing; the app consumes a hand-built DRF API
(ADR-0004). Tree position carried no meaning — every relationship
that matters (source, themes, tags) is already an explicit FK/M2M.
The build is at scaffold stage: no environment holds real content, so a destructive schema change is cheap now and expensive later.
Decision Drivers¶
- Must: editors get flat per-type listings with status columns (mockup); draft/live + revisions + moderation retained (AI-generated content must stay unreviewable-by-default, constraint #3).
- Must: no dead structural concepts for editors to trip on (index pages, tree hierarchy).
- Should: land ADR-0006 decision 1 (unified
Writing) in the same schema pass.
Considered Options¶
Option A: Keep Pages, customize the admin around the tree¶
- Pros: no schema change; page URLs if ever needed.
- Cons: explorer navigation fights the mockup; index/root pages are pure ceremony; slugs are
tree-scoped;
PageChooserBlocks couple content towagtailcore.Page.
Option B: Snippets with Wagtail feature mixins (chosen)¶
- Pros: flat
SnippetViewSetlistings +SnippetViewSetGroupsidebar groups map 1:1 to the mockup;DraftStateMixin/RevisionMixin/WorkflowMixin/LockableMixinretain publish workflow; models become plain FK-addressable rows (RAPID-friendly). - Cons: no per-item URL routing or
Page.copy-style tree tooling (not needed headless); title/slug become explicit model fields.
Decision¶
Passage,Book, and the newWritingare snippets built onWorkflowMixin + DraftStateMixin + LockableMixin + RevisionMixin + index.Indexed + ClusterableModel, each with explicittitle+ uniqueslug.WritingreplacesLetter/Essaywith akinddiscriminator (letter/essay/chapter) and per-kindclean()validation — ADR-0006 decision 1, realised as a snippet.collected_in(M2M) is dropped for a singlesourceFK →Book.Passagecarries two nullable source FKs —source→Bookandsource_writing→Writing(was FK →wagtailcore.Page+ content-type validation). A passage is drawn from a single origin — a Book xor a Writing (letter/essay/chapter), never both — matching the documented source chain (data-model plan);clean()enforces the xor. Two explicit FKs rather than aGenericForeignKeykeep native per-type snippet choosers,select_related, and admin list filtering. The importer only ever setssource(Book) today.- The page tree dies:
SiteRoot, the four index pages, and theHomePageplaceholder are deleted. The Home surface will be rebuilt as its own model against the six-slot feed contract (CSL-42) when curation work starts — the placeholder (intro +PageChooserBlocks) matched nothing in that contract and its chooser blocks break once targets aren't Pages. - Admin: content and taxonomy snippets are flat top-level menu items grouped visually by CSS
section headers (Content / Taxonomy / Media) — Wagtail's sidebar has no native section header,
so each zone's first item carries the label (
classname+data-zone-label, styled inadmin_theme.css). The page explorer menu item is hidden and the site root/redirects to the CMS sign-in. - Migrations are destructive (
0004deletes the Page models,0005creates the snippet tables) — acceptable because nothing beyond re-runnable imports exists in any environment. The Mere Christianity import re-runs cleanly against the new schema.
Consequences¶
Positive¶
- Admin now matches the editorial mockup with stock Wagtail (no custom UI).
- Content rows are plain models: simpler queries in
readers, nopage_ptrjoins, no.specificcasts, slugs globally unique. seedshrinks to dev users only; the importer no longer needs a pre-built tree.
Negative¶
- Re-adding page-served content later would mean re-introducing Page models (unlikely: headless).
- Wagtail's cross-page reference tooling (
PageChooserBlock, page search in one index) no longer applies; cross-content references must be explicit FKs orSnippetChooserBlocks.
Risks & Mitigations¶
- Risk: future curated surfaces (Home, Journeys) assume Pages. → Build them as snippets/models against their contracts; nothing in the app consumes Wagtail routing.
- Risk: dropped
Letter/Essaytables surprise a stale environment. → Only dev existed, with zero content rows; migrations apply cleanly in sequence.