Skip to content

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.tsx and 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)

  1. Phase 1 (this doc): structured field editor in the admin panel — pick a page, edit its slots as labeled fields, save.
  2. Later: in-page “Edit” affordance (admin-only) that opens the same structured editor for the slot in context.
  3. 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/authenticated may select; only admins may insert/update.
  • History rows written by an after update trigger on content_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.field dot-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)

  1. Stats are editable copy. Marketing claims (8,667 lawsuits, $1/page, <60s, $7.50–$11.50) become editable stat slots (number + label). The accessibility page’s “Latest evaluation” date stays computed (not a slot).
  2. Guide article bodies → Phase 2. Long-form guide content moves to a separate guide_articles table with a markdown/rich-text body, not the flat slot editor. Out of scope for Phase 1.
  3. Form labels stay in locale files. Contact / SSO intake labels belong in next-intl locale files (i18n), not the CMS.
  4. Alt text is in scope. Image alt is surfaced as its own labeled alt-text slot (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

TierPagesSlots
1 — BUILThome (72), why-gevell (19), about (25)116
2 — planneddata-privacy (~37), workshops (~24), accessibility (~14), PlanTiers (~6)~81
3 — plannedchecklist (~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_type legend: st=short-text, lt=long-text, cta=cta-label, stat=stat (number/label), li=list-item, alt=alt-text.

home (/) — 47

slottypedefault (truncated)
hero.eyebrowstThe Accessibility Compliance Platform
hero.h1_line1stEvery Document, Made Accessible.
hero.h1_line2_highlightstAffordable. Attractive. Honest.
hero.bodyltDocuments, forms, presentations, and org charts — Gevell brings your…
hero.cta_buttonctaScan your site free
hero.cta_subtextstFree WCAG 2.1 AA scan — free account, no card required.
hero.primary_link_1ctaExplore the tools
hero.primary_link_2ctaTalk to sales
hero.lawsuit_stat_numberstat8,667
hero.lawsuit_stat_labelstatADA lawsuits filed in 2025 alone.
hero.lawsuit_stat_bodyltADA Title II applies today — the new DOJ rule just sets…
social_proof.eyebrowstBuilt for government & higher education
social_proof.orgs.1..5liState agencies / Public universities / School districts / Cities & counties / Community colleges
reports.eyebrowstSee the receipts
reports.h2stDon’t take our word for it. Read a real report.
reports.bodyltEvery document leaves with the same honest paper trail…
pillars.h2stAccessible. Affordable. Attractive.
pillars.introltCompliance shouldn’t mean stripped-down files, blown budgets…
pillars.pillar_1_title / _bodyst/ltAccessible / Every file is brought to WCAG 2.1 AA…
pillars.pillar_2_title / _bodyst/ltAffordable / Automated remediation starts at $1 a page…
pillars.pillar_3_title / _bodyst/ltAttractive / Your layout, fonts, and branding survive…
pillars.honest_callout_h3 / _bodyst/ltAn honest account — not a rubber stamp / We don’t certify…
products.h2 / .bodyst/ltOne Platform. Every Kind of File. / Compliance isn’t just your PDFs…
why_now.h2 / .bodyst/ltWhy This Can’t Wait / ADA Title II has applied…
why_now.stat_1..3 (number/label/desc)stat/lt8,667 · $7.50–$11.50 · <60s (+ labels + descriptions)
pricing.h2 / .bodyst/ltFree to start. A plan when you’re ready. / Audits and ACR grading are free…
pricing.free_badge / _title / _subtext / _notest/ltFree for everyone / Tools you can use right now…
contact.h2 / .bodyst/ltNeed volume pricing or a procurement quote? / We work with state agencies…
contact.feature.1..5liPO/invoice billing · SSO · ACR/VPAT access · bundled pricing · integration support
contact.email_prefixstPrefer email? Reach us at
final_cta.h2 / .bodyst/ltDon’t Wait for a Lawsuit / 8,667 ADA lawsuits were filed in 2025…
final_cta.button_1 / _2ctaExplore the Tools / Request a Quote
a11y_statement.h2 / .bodyst/ltAccessibility 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

  1. Migration: content_blocks + content_block_history + RLS + trigger.
  2. getContent(page, slot, default) helper in apps/home (reads store, falls back to in-code default) + route revalidation on save.
  3. Wire Tier 1 slots (home, why-gevell, about).
  4. Admin editor screen (structured fields, optimistic save, axe test in suite).
  5. Tiers 2 and 3.
  6. Phase 2: guide-article rich-text system.