Appearance
The content model (one model, two dimensions)
NanaSelect has one content model for app copy, made of two deliberate, non-overlapping dimensions. This page is the reference for how they fit together, why there are two, and what happened to the original slot-based system after the pages CMS landed (issue #677; decision #693).
The two dimensions are unified — for editors — by the single content inventory at /admin/content, which lists both in one place with their publish state.
The two dimensions
| Dimension | What it is | Route | Edited in |
|---|---|---|---|
| CMS pages (#673–#675) | Admin-authored standalone pages composed of typed sections | /<slug> (derived from the slug) | Pages + the content inventory |
| Code-routed slots (#548–#551) | Named editable copy embedded in a code-owned route (the home hero headline/body) | Fixed, owned by app code (e.g. /) | Page content |
The distinction is structural, not incidental:
- A CMS page is a whole page whose existence, URL, and body are data. It is the right model when an editor needs to create a new page that did not exist in code.
- A code-routed slot is a small piece of copy inside a page that already exists in code and does other things. The home front door (
/) is education-first and static-first — it renders a hero, how-it-works, persona tracks, a right-sizing story, and a resume affordance, and it degrades gracefully with no database at all (home-page reference). Its editable hero copy is a slot; the page itself is code.
Convergence: why there are still two (and why that's correct)
When the pages CMS (#673–#675) arrived, the question was whether to fold the older slot system into it. The answer — recorded as decision #693 — is that the two dimensions are one model with two roles, not duplication to be collapsed:
- The only live slot copy is the home hero, on the code-owned
/route. There is no clean CMS-page target for it, and migrating live front-door copy would risk a regression for no structural gain. - So each code-routed page that owns slots declares an explicit convergence disposition in
src/lib/content-convergence.ts— todayhome → code-owned-bridge, with the reason it stays slot-owned. A completeness invariant (isConverged, enforced by test) means a newly-registered slot on an undeclared page fails the build until its place in the model is stated. The two dimensions therefore cannot drift back into an undocumented split.
The fate of the legacy slot system: retained, not deprecated
The #548 page_content store and the /admin/page-content editor are kept, on purpose — they are the mechanism for editable copy embedded in code-owned routes, a role CMS pages do not fill. They are not a legacy remnant scheduled for removal. The content inventory cross-links to the page-content editor (the Edit content action) so the retained editor is reachable from the one content home.
Guardrail continuity (#551)
Unifying the model did not open any path from editable content to entity-derived data (products, attributes, factors, media, Drupal). This holds structurally, and is regression-proofed by tests:
- CMS page slugs can never shadow a reserved app/entity route (
select,compare,admin,api,media,health) — rejected on both the write path (isValidSlug) and the public read path (isRenderableSlug). - CMS sections carry only plain
text/textareafields — no field kind holds markup or an entity reference (the #551 no-markup guarantee, extended to sections). - The slot store still rejects any unregistered / entity-shaped address (#551).
See src/lib/content-convergence.guardrail.spec.ts and src/lib/server/page-content.parity.spec.ts for the enforced guarantees.