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

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 & taglinetemplate-resolver, editable copy, keyed by slug, changed without a deploy.
  • Meta title & descriptiontemplate-resolver, 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 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" },
};
}

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 (click .shot-frame to enlarge).
  • 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.

Screenshots inside .shot-frame (hero and custom html showcase rows) open in a built-in lightbox, vanilla <dialog>, no React:

Apex lightbox open after clicking the hero screenshot

Details: Dialog and Lightbox overlays.