Multi-page Pro Website — Acceptance Spec
Status: APPROVED 2026-05-21 — founder-confirmed. This spec is the source of truth: code that doesn't match it is wrong, and tests assert it.
Scope: Let a Pro Website customer add a small number (≤ 8) of separate pages — About, Ministries, Sermons, Contact, or a blank custom page — to what is today a single anchor-scroll page. A real multi-page site, not a CMS. Covers the page model, the public render at /s/[slug]/[page], the "Pages" group in the Website editor, and how pages ride the existing draft/publish flow.
Architecture source: knowledge/drafts/2026-05-21-pro-website-multipage-scoping.md (full options analysis + 14-point regression-risk table).
Foundational decisions
- Standard, not an add-on (founder decision 2026-05-21). Multi-page is included in every Pro Website plan (site-only $14.95, bundled $19.95) — no entitlement flag, no separate Stripe price, no tier gating. Available to every Pro Website subscriber. No
pricing.yaml/PRICING.md/ Stripe change. - Page cap is a flat 8 (founder decision 2026-05-21). Every Pro Website plan gets up to 8 extra pages. The home page is not counted.
- Additive data model — the one-page site never regresses. Two new nullable
jsonbcolumns (extra_pages,draft_extra_pages), defaultnull. A church that never adds a page renders byte-identically to today. No data migration, no enum change, reversible. - Constrained page model, not free-form HTML. Each extra page is an ordered list of a small fixed set of block types. No arbitrary widgets, no custom CSS, no scripts, no inline rich-text markup. This keeps every page on-brand and off the XSS surface.
- One site-wide draft/publish. Extra pages ride the existing
premium_churchesdraft cycle. One Publish button publishes the home page and all extra pages together. No per-page publishing (that is the CMS complexity deliberately scoped out).
Tier visibility
| Tier | "Pages" editor group + multi-page render? |
|---|---|
| Pro Website site-only ($14.95) | YES |
| Pro Website bundled with Chat ($19.95) | YES |
| Chat / Voice / Suite / Bundle tiers (no Pro Website) | N/A — no Website editor; /s/[slug]/[page] 404s |
| ITW / SermonWise / PewSearch Premium | N/A — different products |
The page model
A single canonical column extra_pages jsonb holds ExtraPage[]. Its draft twin is draft_extra_pages.
ExtraPage = {
id: string // stable uuid, never reused
slug: string // url segment, lowercase a-z0-9-, 2-40 chars, unique within the site
title: string // <title> + on-page header, ≤ 80 chars
nav_label: string // nav link text, ≤ 24 chars
sort_order: number // nav + editor ordering
show_in_nav: boolean // when false, page renders + is linkable but is not in the nav
blocks: Block[] // ≤ 12 blocks
}
Block is a discriminated union of exactly 8 types:
Block type | Fields | Renders |
|---|---|---|
heading | text (≤ 120 chars), level (2 | 3, default 2) | A section heading in the page's denomination style |
text | text (≤ 2,000 chars, plain text — line breaks preserved, no HTML) | A paragraph block; blank lines split paragraphs |
image | url, alt (≤ 160), caption (≤ 200, optional) | A centered, rounded image with optional caption |
button | label (≤ 40), href | A branded call-to-action button (http/https/tel/mailto/anchor only) |
staff_grid | — | The church's existing custom_staff, same cards as the home Staff section |
ministries_grid | — | The church's existing custom_ministries, same cards as the home Ministries section |
sermons_list | — | The church's existing sermons, same layout as the home Sermons section |
contact_block | — | The same ContactForm the home page uses (church-scoped) |
Hard limits, enforced server-side in normalizeExtraPages() and client-side in the editor: ≤ 8 pages, ≤ 12 blocks per page, the per-field char caps above. Anything over the cap is rejected on save with a clear message; anything malformed is dropped defensively on render (never throws into the render path).
Routing & URLs
- Home page is unchanged:
/s/[slug](subdomainslug.john316.church/). - Extra page:
/s/[slug]/[page](subdomainslug.john316.church/<page>). New route file; the home route file is untouched. - The middleware subdomain rewrite (
*.john316.church/<path>→/s/<slug>/<path>) already preserves the path — no middleware change. revalidate = 3600(ISR), same as the home route.- Reserved slugs the editor blocks and the route never serves as an extra page:
privacy,terms,admin,api,_next,sitemap,robots,favicon, and the home-page section anchors (about,times,hours,office-hours,expect,beliefs,team,ministries,events,map,contact-form,giving). The literalprivacy/termsroute folders win by Next.js routing priority regardless. - A
/s/[slug]/[page]request for a slug not present in the site's publishedextra_pagesreturns 404 (notFound()). - Links are root-relative (
/about,/), matching the established/privacy+/terms+ footer convention and the subdomain serving model.
Expected output — public visitor
A church with no extra pages (the default — 100% of existing customers)
/s/[slug]renders byte-identically to before this feature shipped. Nav, footer, sections — all unchanged./s/[slug]/anythingreturns 404.
A church with published extra pages
- The home nav (
StickyNav) shows the extra pages first, then the existing in-page section anchors. Overflow beyond 5 links collapses into the existing "More" dropdown. - Each extra page at
/s/[slug]/[page]:- Reuses the same denomination styling,
StickyNav, andTemplateFooteras the home page — a visual sibling of the home page. - Shows a modest header strip (the page
title), lighter than the home video hero. - Renders its blocks top-to-bottom through
BlockRenderer. StickyNavis always visible on extra pages (a short page may never scroll past the home page's 200px reveal threshold). Its logo links to/(home). Its links areHome+ the other extra pages.- The footer shows the extra-page links alongside Privacy / Terms / "Powered by ChurchWiseAI".
- Reuses the same denomination styling,
- A page with
show_in_nav = falsestill renders and is still reachable by direct link; it just is not listed in the nav. generateMetadataper page:title= the page title,index: true(extra pages are indexable, unlike/privacy+/terms).
Expected output — pastor in the editor
In the Website tab editor (WebsiteTabEditor.tsx), a new "Pages" sidebar group sits below "Sections" / "Design":
- Lists Home (pinned, not deletable, not reorderable) then each extra page, each with a drag handle and a settings control.
- An "Add page" action (the previously-disabled "Add Section" placeholder button is now enabled) opens a small modal: 4 starter presets — About, Ministries, Sermons, Contact — plus Blank. Each preset pre-seeds sensible blocks so a non-technical pastor never faces an empty canvas.
- Choosing a preset/Blank creates the page; its
slugauto-derives from the title with the existing debounced uniqueness check, and reserved slugs are rejected inline. - Editing a page opens the block editor (in the existing
SectionEditorSheet): a vertical list of block cards + an "Add block" picker offering the 8 block types. Each block type has a small form (heading text, paragraph text, image upload, button label/href). The 4 projection blocks (staff_grid,ministries_grid,sermons_list,contact_block) have no fields — they show a one-line "pulls from your Staff / Ministries / Sermons / Contact form" note. - Pages and blocks reorder by drag (up/down arrows on mobile). Rename warns that old links will break. Delete asks for confirmation.
- All page edits flow through
DraftAwareSaveForm→/api/premium/update(websitesection,target=website_draft). They land ondraft_extra_pages, the draft pill shows "unpublished changes", and the preview iframe reloads — exactly like every other Website edit. - Clicking Publish copies
draft_extra_pages → extra_pages(and every other draft column) atomically. Extra-page ISR paths are revalidated so the new/changed pages go live immediately.
Draft / publish contract
draft_extra_pages → extra_pagesis registered inDRAFT_TO_CANONICAL,DRAFT_SELECT_COLUMNS, andProWebsiteDraftColumns(src/lib/pro-website-draft.ts). The publish route iteratesDRAFT_TO_CANONICAL, so publishing extra pages needs no publish-route logic change beyond revalidation./api/premium/update: thewebsitesection gains an additiveextra_pagesfield, parsed/validated throughnormalizeExtraPages()behind its ownformData.has('extra_pages')guard. The existing events-parse block (the one carrying thename-vs-titlefix) is not touched.- The
?draft=<admin_token>preview overlaysdraft_extra_pagesontoextra_pagesfor both the home route (nav) and the[page]route (content), keyed on the admin-token match — same mechanism as every other draft column. - Publish revalidates
/s/[slug]and every/s/[slug]/<page-slug>path.
Regression guardrails (must hold)
- A church with
extra_pages = nullrenders/s/[slug]byte-identically — verified by a no-regression snapshot test. - The home route file (
s/[slug]/page.tsx) only gains the additive nav extension + the additiveextra_pagesdraft-overlay line — both no-ops whenextra_pagesis null. UnifiedTemplate,StickyNav,TemplateFootereach gain only optional props that default to the pre-feature behavior.service_business/ funeral demo pages never renderSimplePageTemplate— the[page]route is gated to non-service_business, Pro-Website-plan churches, with the same orphan-FKnotFound()guard as the home route.- Critical-path Playwright suites
cwa-pro-website-ssrandcwa-pro-website-editrun green; new specs are added (single-page no-regression, multi-page render, page save→publish) rather than altering existing assertions. Apro-website-multipageregistry entry is added.
Out of scope (v1)
- Per-page publishing, per-page analytics, page version history.
- Inline rich text (bold/italic/links inside a paragraph) —
textblocks are plain text only. - Nested layouts, multi-column blocks, custom CSS, embedded scripts.
- Planning Center calendar content as a page — when PC integration composes here, it arrives as a future block type in this same union, not a parallel page system (the two scopes agreed: page model stays additive).