Marketing Content CMS — apps/home
A lightweight, Supabase-backed content store that lets a non-technical admin
(Laura) edit the marketing copy on gevell.com (apps/home) without a code
change or redeploy. Structured slots only — never raw HTML — so WCAG 2.2 AA
structure is preserved by construction.
Why this shape
- Content currently lives in code (hardcoded strings in
page.tsxand friends). The store overrides those strings; the code strings remain the fallback default, so an empty store changes nothing. - Structured slots, not raw HTML. A slot is one editable string dropped into
fixed markup. Laura can’t break heading order, drop an
alt, or ship failing contrast, because only text flows into a fixed structure. - Supabase, not KV — queryable, an audit trail, and easy diff/rollback.
Editing surfaces (roadmap)
- Phase 1 (this doc): structured field editor in the admin panel — pick a page, edit its slots as labeled fields, save.
- Later: in-page “Edit” affordance (admin-only) that opens the same structured editor for the slot in context.
- Later: chat layer (“change the hero subtitle to X”) writing to the same slots, always with preview + confirm.
Schema
create table content_blocks ( id uuid primary key default gen_random_uuid(), app text not null default 'home', page text not null, -- 'home','about','why-gevell', ... slot text not null, -- 'hero.headline' field_type text not null, -- short-text|long-text|cta-label|stat|list-item|alt-text value text not null, updated_at timestamptz not null default now(), updated_by uuid references auth.users(id), unique (app, page, slot));
create table content_block_history ( -- audit + revert id uuid primary key default gen_random_uuid(), block_id uuid references content_blocks(id) on delete cascade, old_value text, new_value text, changed_at timestamptz not null default now(), changed_by uuid references auth.users(id));- RLS:
anon/authenticatedmayselect; only admins mayinsert/update. - History rows written by an
after updatetrigger oncontent_blocks. - A save triggers a lightweight revalidate of the affected route (ISR / tag-based) rather than a full redeploy.
Slot key convention
page= route slug:home,about,why-gevell,data-privacy,workshops,accessibility,checklist,integrations-s3,mcp,cicd,sso,guides,header(shared).slot=section.fielddot-path:hero.headline,pricing.free.title,tiers.1.description,contact.feature.2.- Access in code:
getContent(page, slot, defaultValue)— override wins when a row exists, otherwise the in-code default renders.
Scope decisions (2026-09-10, Larry)
- Stats are editable copy. Marketing claims (
8,667 lawsuits,$1/page,<60s,$7.50–$11.50) become editablestatslots (number + label). The accessibility page’s “Latest evaluation” date stays computed (not a slot). - Guide article bodies → Phase 2. Long-form guide content moves to a
separate
guide_articlestable with a markdown/rich-text body, not the flat slot editor. Out of scope for Phase 1. - Form labels stay in locale files. Contact / SSO intake labels belong in next-intl locale files (i18n), not the CMS.
- Alt text is in scope. Image
altis surfaced as its own labeledalt-textslot (Tier 1).
Off-limits (never editable here)
Product registry (packages/accessible-org-ui/src/products.ts) · live workshops
catalog (WorkshopsDynamic.tsx) · legal document bodies (src/lib/legal.ts,
/legal/[slug]) · nav route labels · shared footer.
Rollout tiers
| Tier | Pages | Slots |
|---|---|---|
| 1 — BUILT | home (72), why-gevell (19), about (25) | 116 |
| 2 — planned | data-privacy (~37), workshops (~24), accessibility (~14), PlanTiers (~6) | ~81 |
| 3 — planned | checklist (~42), integrations-s3 (~23), mcp (~17), cicd (~17), sso (~6), guides (~3), header (~4) | ~112 |
Tier 1 counts are the as-built figures from the manifest. Tiers 2/3 are planning estimates and will be replaced with as-built counts as they ship. When a paragraph carries an inline link or emphasis it is split into fragment slots, so as-built counts run higher than the flat first-pass estimates.
Slot inventory — source of truth
The manifest apps/home/public/content-slots.json is the authoritative list
of built slots (key, label, type, default). Do not re-transcribe it here — a
duplicated table only rots. To see every current slot:
cat apps/home/public/content-slots.json | jq '.pages[] | {page, count: (.slots|length), slots: [.slots[].slot]}'The section below is the original first-pass plan, kept for the naming rationale and the Tier 2/3 scope. It predates implementation and does not match the built manifest slot-for-slot (e.g. paragraphs with inline links were split into fragment slots; some names changed). Trust the manifest, not the tables below, for built pages.
field_typelegend:st=short-text,lt=long-text,cta=cta-label,stat=stat (number/label),li=list-item,alt=alt-text.
home (/) — 47
| slot | type | default (truncated) |
|---|---|---|
| hero.eyebrow | st | The Accessibility Compliance Platform |
| hero.h1_line1 | st | Every Document, Made Accessible. |
| hero.h1_line2_highlight | st | Affordable. Attractive. Honest. |
| hero.body | lt | Documents, forms, presentations, and org charts — Gevell brings your… |
| hero.cta_button | cta | Scan your site free |
| hero.cta_subtext | st | Free WCAG 2.1 AA scan — free account, no card required. |
| hero.primary_link_1 | cta | Explore the tools |
| hero.primary_link_2 | cta | Talk to sales |
| hero.lawsuit_stat_number | stat | 8,667 |
| hero.lawsuit_stat_label | stat | ADA lawsuits filed in 2025 alone. |
| hero.lawsuit_stat_body | lt | ADA Title II applies today — the new DOJ rule just sets… |
| social_proof.eyebrow | st | Built for government & higher education |
| social_proof.orgs.1..5 | li | State agencies / Public universities / School districts / Cities & counties / Community colleges |
| reports.eyebrow | st | See the receipts |
| reports.h2 | st | Don’t take our word for it. Read a real report. |
| reports.body | lt | Every document leaves with the same honest paper trail… |
| pillars.h2 | st | Accessible. Affordable. Attractive. |
| pillars.intro | lt | Compliance shouldn’t mean stripped-down files, blown budgets… |
| pillars.pillar_1_title / _body | st/lt | Accessible / Every file is brought to WCAG 2.1 AA… |
| pillars.pillar_2_title / _body | st/lt | Affordable / Automated remediation starts at $1 a page… |
| pillars.pillar_3_title / _body | st/lt | Attractive / Your layout, fonts, and branding survive… |
| pillars.honest_callout_h3 / _body | st/lt | An honest account — not a rubber stamp / We don’t certify… |
| products.h2 / .body | st/lt | One Platform. Every Kind of File. / Compliance isn’t just your PDFs… |
| why_now.h2 / .body | st/lt | Why This Can’t Wait / ADA Title II has applied… |
| why_now.stat_1..3 (number/label/desc) | stat/lt | 8,667 · $7.50–$11.50 · <60s (+ labels + descriptions) |
| pricing.h2 / .body | st/lt | Free to start. A plan when you’re ready. / Audits and ACR grading are free… |
| pricing.free_badge / _title / _subtext / _note | st/lt | Free for everyone / Tools you can use right now… |
| contact.h2 / .body | st/lt | Need volume pricing or a procurement quote? / We work with state agencies… |
| contact.feature.1..5 | li | PO/invoice billing · SSO · ACR/VPAT access · bundled pricing · integration support |
| contact.email_prefix | st | Prefer email? Reach us at |
| final_cta.h2 / .body | st/lt | Don’t Wait for a Lawsuit / 8,667 ADA lawsuits were filed in 2025… |
| final_cta.button_1 / _2 | cta | Explore the Tools / Request a Quote |
| a11y_statement.h2 / .body | st/lt | Accessibility Statement / We pride ourselves on WCAG 2.1 AA… |
why-gevell — 20
hero.eyebrow, hero.h1_line1, hero.h1_line2_highlight, hero.body, what_changed.h2, what_changed.intro, what_changed.item.1..4, name.h2, name.paragraph_1, name.paragraph_2, name.tagline, reason.h2, reason.paragraph_1, reason.paragraph_2, cta.h2, cta.body, cta.button.
about — 18
hero.eyebrow, hero.h1, hero.body, profile.h2, profile.role, profile.paragraph_1..4, credentials_heading.h2, credentials_heading.body, timeline.h2, speaking.h2, speaking.body, mission.h2, mission.body, mission.button_1, mission.button_2.
data-privacy — 37
hero.badge, hero.h1_line1, hero.h1_line2_highlight, hero.body,
hero.privacy_link, tiers.h2, tiers.intro, best_practices.h2,
best_practices.intro, cta.h2, cta.body, cta.button_1, cta.button_2, and
tiers.1..5 each with .title / .subtitle / .description / .result.
workshops — 24
trust_bar.line1/creds/line3, discount_band.title/body, workshops_section.divider_label/h2/intro, acr_banner.h3/body, trainings_section.divider_label/h2/intro, outcomes.eyebrow/h2/intro, who.eyebrow/h2/intro, instructors.eyebrow/h2/intro, cta_band.h2/body. (Dates, prices, cards remain dynamic via WorkshopsDynamic.)
accessibility — 14
hero.eyebrow/h1/body, conformance.standard_value/scope_value/status_value, results.h2/body, results.manual_checks_h3, principles.h2, products.h2/body, feedback.h2/body. (Latest-evaluation date stays computed.)
checklist — 42
hero.eyebrow/h1/body, callout.h2/body, and step.1..10 each with .title
plus its list items (step.N.item.M).
integrations-s3 — 23
hero.badge/h1/body, how_it_works.h2/paragraph_1/paragraph_2, providers.h2/intro, table.provider_col/auth_col/setup_col/event_col/note, capabilities.h2/intro, setup.h2/body/step_1/step_2/step_3, cta.h2/body/button.
mcp — 17
hero.badge/h1/body, what_it_is.h2/paragraph_1/paragraph_2, capabilities.h2/intro, install.h2/body/step_1/step_2/step_3, cta.h2/body/button.
cicd — 17
hero.badge/h1/body, how_it_works.h2/paragraph_1/paragraph_2, capabilities.h2/intro, setup.h2/body/step_1/step_2/step_3, cta.h2/body/button.
sso — 6
hero.badge/h1/body (+ intake form labels stay in locale files).
guides (listing) — 3
hero.eyebrow, hero.h1, hero.body. (Article bodies → Phase 2.)
header (shared) — 4
logo.alt_text (alt), nav.help_link_label, mobile_nav.toggle_label (alt). (Product/nav route labels off-limits.)
Alt-text slots (decision 4)
Any image with a meaningful alt gets an alt-text slot alongside its section
(e.g. hero.image_alt, reports.screenshot_alt). Decorative images
(alt="" + aria-hidden) are not slots.
Build order
- Migration:
content_blocks+content_block_history+ RLS + trigger. getContent(page, slot, default)helper in apps/home (reads store, falls back to in-code default) + route revalidation on save.- Wire Tier 1 slots (home, why-gevell, about).
- Admin editor screen (structured fields, optimistic save, axe test in suite).
- Tiers 2 and 3.
- Phase 2: guide-article rich-text system.