Skip to content

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 stateVisitorAuthenticated admin
PublishedRendersRenders (+ an admin edit link)
DraftUnavailable — reported as an unknown slug (existence not leaked)Preview at the real URL, under a banner
RetiredUnavailable — honest "retired" copyPreview at the real URL, under a banner
Unknown slugUnavailable — "not found" copySame

"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: SectionRenderer renders nothing for it (never a 500). RENDERABLE_SECTION_TYPES is 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.