Build a marketing landing page
A marketing landing page is mostly static: a hero, a few feature cards, a
pricing table, a call to action. renderApexPage turns a typed ApexPage
description into one complete HTML string, server-side, zero React, one
cacheable HTTP response. No build step, no hydration.
The renderer lives in @cosmicdrift/kumiko-headless/apex. It ships two themes
(light and dark) and a fixed set of section kinds; your app passes only data
and its brand tokens.
Compose it from your features
Section titled “Compose it from your features”Keep landing-page content in the features that own it. Hard-coded copy and prices go stale, which makes the marketing page drift from the product. Pull each piece from its existing feature:
- Hero headline & tagline ←
template-resolver, editable copy, keyed by slug, changed without a deploy. - Meta title & description ←
template-resolver, the same blocks, reused for the page title and SEO. - Prices & per-month suffix ←
tier-engine, the amounts from your plan config, one source of truth. - Plan caps (“50 projects”) ←
tier-engine/ cap config, the same limits the app enforces. - Sign-in / sign-up links ←
auth-email-password, the real auth routes.
The renderer stays dumb on purpose. It does not fetch, query, or know about any feature, your app reads those features and hands the renderer plain data. That keeps the page a pure function of its inputs, and trivially testable.
The two seams
Section titled “The two seams”Hero copy from template-resolver. Look up each block by slug, with a baked-in
fallback so the page renders fully even before anything is seeded:
function block(blocks: ContentBlocks | undefined, slug: string, fallback: string): string { return blocks?.get(slug) ?? fallback;}
// in the hero section:title: block(input.blocks, "index:hero.title", "Ship your roadmap, not your spreadsheet"),Prices from tier-engine. Map each plan from your tier config onto a
pricing card, the amount, the per-month suffix, and the cap all come from the
config the app already enforces, so the table can’t lie:
function toPricingTier(plan: PlanInfo): ApexPricingTier { const paid = plan.monthlyEur !== null; return { name: plan.name, amount: paid ? formatEuro(plan.monthlyEur) : "0 €", priceSuffix: paid ? "/month" : undefined, capLine: Number.isFinite(plan.maxProjects) ? `${plan.maxProjects} projects` : "Unlimited projects", benefits: plan.benefits, cta: { label: `Choose ${plan.name}`, href: "/signup" }, };}Try it
Section titled “Try it”The full, runnable example, both seams, a test that proves the fallback and
the price formatting, and the screenshot above, is the
apex-landing recipe. It embeds its own
source, so the code on that page is exactly what runs.
The screenshot is not hand-captured: bun run screenshot renders the sample
page and shoots it with Playwright (page.setContent, no server needed,
because the renderer is pure). CI regenerates it with SCREENSHOT_DIR pointed
at public/screenshots/apex/, so the published image can’t drift from the
renderer; a plain local bun run screenshot writes to the recipe’s own
screenshots/ folder for a quick preview.
Section kinds
Section titled “Section kinds”An ApexPage is a head, a header, a footer, and an ordered list of
sections. Each section is one of:
hero, logo, headline, tagline, CTAs, optional screenshot (click.shot-frameto enlarge).feature-grid, eyebrow, heading, and a grid of icon + title + text cards.pricing-grid, the plan cards, oneApexPricingTiereach.info-grid, a lighter grid for trust / FAQ / “how it works” blurbs.final-cta, closing headline and a single call to action.html, an escape hatch for one app-specific section of raw HTML.
Themes switch on theme: "light" | "dark"; both CSS sets ship and the body
class toggles. Brand colours come from brand.tokensCss, the :root token
block your app already owns, passed through verbatim.
Screenshots inside .shot-frame (hero and custom html showcase rows) open
in a built-in lightbox, vanilla <dialog>, no React:
Details: Dialog and Lightbox overlays.