Skip to content

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.

Rendered Apex landing page — hero, feature grid, pricing table, final call to action

The point isn’t the HTML — it’s where the content comes from. A landing page is the worst place to hard-code copy and prices: they go stale, and the marketing page quietly drifts from the actual product. Instead, pull each piece from the feature that already owns it:

  • Hero headline & taglinetext-content — editable copy, keyed by slug, changed without a deploy.
  • Meta title & descriptiontext-content — the same blocks, reused for the page title and SEO.
  • Prices & per-month suffixtier-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 linksauth-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.

Hero copy from text-content. 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" },
};
}

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.

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.
  • feature-grid — eyebrow, heading, and a grid of icon + title + text cards.
  • pricing-grid — the plan cards, one ApexPricingTier each.
  • 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.