Appearance
CMS public rendering (reference)
How an admin-authored CMS page (#673) and its sections (#674) reach a visitor at /<slug> (issue #675). For authoring, see managing-pages.md.
The catch-all route
Public pages are served by a single lowest-priority catch-all route, src/routes/[...cmsPath]/+page.server.ts. SvelteKit resolves every specific route before a [...rest] catch-all, so a real app route or entity surface — /admin/…, /api/…, /select/<track>, /compare, /health, /media/…, the home page — is matched first and the catch-all never sees it.
A second guard, isRenderableSlug (in the client-safe $lib/cms-route.ts), is the belt to that suspenders: only a single, non-reserved, kebab-case segment is a candidate page slug. A multi-segment path (a/b), a malformed segment, or a reserved word (select, compare, admin, api, health, media) is refused without a database read. The same RESERVED_PAGE_SLUGS list also forbids authoring a page on a reserved slug, so the guard can never actually be reached by a real page — it exists so the invariant is enforced in one place and can't regress.
Visibility
Resolution mirrors tracks (/select/<slug>), via the pure resolveCmsRoute(page, isAdmin):
| Page state | Visitor | Authenticated admin |
|---|---|---|
| Published | Renders | Renders (+ an admin edit link) |
| Draft | Unavailable — reported as an unknown slug (existence not leaked) | Preview at the real URL, under a banner |
| Retired | Unavailable — honest "retired" copy | Preview at the real URL, under a banner |
| Unknown slug | Unavailable — "not found" copy | Same |
"Unavailable" is a friendly notice with a link home (HTTP 200, consistent with the track surface — not a hard 404). A draft is deliberately indistinguishable from an unknown slug to a non-admin, so a URL can't be used to confirm an unpublished page exists.
What renders
- Only a page's published sections render, in
display_order. A draft (or draft-only) section is omitted — this holds even in admin preview, where the banner (not the section set) conveys that the page itself is draft/retired. - Each section renders through its type component (
$lib/sections/SectionRenderer.svelte→ Hero / RichText / Cta / Image). Content is the section's published field map. - An unknown or removed section type degrades gracefully:
SectionRendererrenders nothing for it (never a 500).RENDERABLE_SECTION_TYPESis kept in lockstep with the registry, so a type an admin can author always has a renderer.
SEO
The page's <title> is "<page title> · NanaSelect". A <meta name="description"> is derived from the first hero subheading or rich-text body on the page (truncated), when present — a lightweight default until a description field is modeled on the page itself.
Publish-without-deploy
Because rendering reads the section store at request time, publishing a section (or a page) is reflected on the public URL immediately — no code deploy. Saving a section draft changes nothing a visitor sees until it is published; publishing promotes the draft to the served content on the next request.