Skip to content

recipes-apex-landing

bun run screenshot renders the sample page and shoots a full-page PNG via Playwright (page.setContent — no server, because the renderer is pure). The spec also paints a product-board mock (hero-app.png), uses it in the hero .shot-frame, then writes lightbox.png with the overlay open. Docs embed screenshots/landing.png and screenshots/apex/lightbox.png.

buildLandingPage’s head.schemaJson combines organizationSchema + webPageSchema (from the seo feature) into one @graph block — Organization

  • WebPage nodes in a single <script type="application/ld+json">, which renderApexPage already knows how to emit. This is the seam an app extends with faqPageSchema for an FAQ section, or its own custom schema.org type.

The recipe itself has no server (renderApexPage is a pure function — nothing to boot), but src/__tests__/seo-routes.integration.test.ts shows how an app mounts createSeoFeature alongside the thin GET / route that serves renderLanding(...), and exercises /sitemap.xml + /llms.txt as real HTTP requests via setupTestStack.

Terminal window
bun test # pure-function seams + the seo-mounted integration test
bun run screenshot # → screenshots/landing.png + lightbox.png

The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):

// Apex landing page — composed from OTHER features' data.
//
// `renderApexPage(page)` is a pure function: a typed `ApexPage` in, one HTML
// string out. The interesting part is where that page's CONTENT comes from. An
// app does not hard-code its landing copy or prices — it pulls them from the
// features it already runs, so the marketing page can never drift from the
// product:
//
// • hero headline / tagline ← template-resolver (kind `text-block`, keyed by slug)
// • prices and plan caps ← tier-engine (the app's plan config)
//
// `buildLandingPage` below shows both seams. It takes the data those features
// expose and assembles an `ApexPage`; the app serves `renderApexPage(page)` as
// one static, cacheable response.
import { organizationSchema, webPageSchema } from "@cosmicdrift/kumiko-bundled-features/seo";
import {
type ApexPage,
type ApexPricingTier,
renderApexPage,
} from "@cosmicdrift/kumiko-headless/apex";
import { HERO_SCREENSHOT } from "./constants";
// --- Inputs: shapes the surrounding features hand you -----------------------
/** Editable copy as the `template-resolver` feature projects it: a content string
* per stable slug. A real app fills this Map from the template-resolver read model. */
export type ContentBlocks = ReadonlyMap<string, string>;
/** One plan as the app's `tier-engine` config exposes it. */
export type PlanInfo = {
readonly key: string;
readonly name: string;
readonly tagline: string;
/** `null` = free or on-request; the renderer just shows the `amount` text. */
readonly monthlyEur: number | null;
/** Usage cap from the tier config; `Infinity` = unlimited. */
readonly maxProjects: number;
readonly benefits: readonly string[];
readonly featured?: boolean;
};
export type LandingInput = {
/** From `template-resolver`. Omit a slug and the baked-in fallback is used, so the
* page renders fully even before anything is seeded. */
readonly blocks?: ContentBlocks;
/** From `tier-engine`. */
readonly plans: readonly PlanInfo[];
/** Override hero `.shot-frame` src — screenshot runner passes a data: URL. */
readonly heroScreenshot?: typeof HERO_SCREENSHOT;
};
// --- The two feature seams --------------------------------------------------
/** template-resolver seam: block content if seeded, else the fallback baked in here. */
function block(blocks: ContentBlocks | undefined, slug: string, fallback: string): string {
return blocks?.get(slug) ?? fallback;
}
function formatEuro(n: number): string {
return `${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`;
}
function planAmount(plan: PlanInfo): string {
if (plan.monthlyEur === null) return plan.key === "enterprise" ? "Let's talk" : "0 €";
return formatEuro(plan.monthlyEur);
}
function planCap(plan: PlanInfo): string {
return Number.isFinite(plan.maxProjects)
? `${plan.maxProjects.toLocaleString("en-US")} projects`
: "Unlimited projects";
}
/** tier-engine seam: one plan from the config → one Apex pricing card. */
function toPricingTier(plan: PlanInfo): ApexPricingTier {
const paid = plan.monthlyEur !== null;
return {
name: plan.name,
tagline: plan.tagline,
amount: planAmount(plan),
priceSuffix: paid ? "/month" : undefined,
featured: plan.featured,
badge: plan.featured ? "Popular" : undefined,
capLine: planCap(plan),
benefits: plan.benefits,
cta: {
label: plan.key === "enterprise" ? "Contact us" : `Choose ${plan.name}`,
href: plan.key === "enterprise" ? "/contact" : "/signup",
variant: plan.featured ? "primary" : "secondary",
},
};
}
// --- Assembly ---------------------------------------------------------------
const FEATURE_ICON = {
bolt: '<path d="M13 2 4 14h6l-1 8 9-12h-6z"/>',
shield: '<path d="M12 3 5 6v6c0 4 3 7 7 9 4-2 7-5 7-9V6z"/><path d="m9 12 2 2 4-4"/>',
layers: '<path d="m12 3 9 5-9 5-9-5z"/><path d="m3 13 9 5 9-5"/>',
} as const;
const BRAND_TOKENS = `:root{
--bg:#ffffff; --bg-card:#ffffff; --bg-muted:#f6f7f9;
--border:#e6e8ec; --fg:#0f1729; --fg-muted:#475067; --fg-subtle:#6b7280;
--primary:#4f46e5; --primary-hover:#4338ca; --primary-fg:#ffffff;
--accent:#4f46e5; --accent-fg:#ffffff; --accent-hover:#6366f1;
--status-ok:#16a34a; --shadow:0 12px 30px -12px rgba(15,23,42,.25);
--footer-cols:3;
--font-body:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--font-mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
}`;
export function buildLandingPage(input: LandingInput): ApexPage {
return {
theme: "light",
brand: { tokensCss: BRAND_TOKENS },
head: {
lang: "en",
title: block(input.blocks, "index:meta.title", "Tasklane — ship your roadmap"),
description: block(
input.blocks,
"index:meta.description",
"Plan, track and ship product work in one place. Free to start, no credit card.",
),
canonicalUrl: "https://tasklane.example/",
// GEO/AEO seam: schema.org JSON-LD via the seo feature's pure builders
// (organizationSchema/webPageSchema), fed straight into ApexHead —
// renderApexPage already emits the <script type="application/ld+json">
// block, this recipe only supplies the data. Two node types share one
// block via @graph (a single schemaJson can't hold two top-level @type
// values). Site-discovery (sitemap.xml/llms.txt) for an app that also
// mounts managed-pages/legal-pages is demonstrated separately in
// samples/apps/use-all-bundled's createSeoFeature wiring — this recipe
// has no server/feature of its own to attach routes to.
schemaJson: {
"@context": "https://schema.org",
"@graph": [
organizationSchema({ name: "Tasklane", url: "https://tasklane.example/" }),
webPageSchema({
name: block(input.blocks, "index:meta.title", "Tasklane — ship your roadmap"),
url: "https://tasklane.example/",
description: block(
input.blocks,
"index:meta.description",
"Plan, track and ship product work in one place. Free to start, no credit card.",
),
}),
],
},
},
header: {
brand: { href: "/", label: "Tasklane" },
// A dropdown nav entry (kind:"menu") renders an icon/title/desc panel on
// hover + keyboard focus; plain links sit beside it. One typed shape, no
// app CSS — the renderer ships the dropdown styling.
navLinks: [
{
kind: "menu",
label: "Product",
items: [
{
icon: FEATURE_ICON.bolt,
title: "Live planning",
desc: "Reorder, estimate and assign as you type.",
href: "#features",
},
{
icon: FEATURE_ICON.layers,
title: "Portfolio view",
desc: "Every project on one screen, at a glance.",
href: "#features",
},
{
icon: FEATURE_ICON.shield,
title: "Your data, yours",
desc: "EU-hosted, no tracking, export any time.",
href: "#features",
},
],
footer: { label: "See all features", href: "#features" },
},
{ label: "Pricing", href: "#pricing" },
],
actions: [{ label: "Sign in", href: "/login", variant: "link" }],
},
sections: [
{
kind: "hero",
title: block(input.blocks, "index:hero.title", "Ship your roadmap, not your spreadsheet"),
tagline: block(
input.blocks,
"index:hero.tagline",
"Plan, track and ship product work in one place — free to start, no credit card.",
),
ctas: [
{ label: "Start free", href: "/signup", variant: "primary" },
{ label: "See pricing", href: "#pricing", variant: "secondary" },
],
metaHtml: "<strong>Free forever plan.</strong> No credit card required.",
screenshot: input.heroScreenshot ?? HERO_SCREENSHOT,
},
{
kind: "feature-grid",
id: "features",
eyebrow: "Features",
heading: "Everything your team needs to ship",
sub: "From a single backlog to the whole portfolio — without tab-juggling.",
items: [
{
icon: FEATURE_ICON.bolt,
title: "Live planning",
desc: "Reorder, estimate and assign in one board that updates as you type.",
},
{
icon: FEATURE_ICON.layers,
title: "Portfolio view",
desc: "Every project on one screen: progress, owners and ship dates at a glance.",
},
{
icon: FEATURE_ICON.shield,
title: "Your data, yours",
desc: "EU-hosted, no tracking, export any time. Privacy is the baseline, not a tier.",
},
],
},
{
kind: "pricing-grid",
id: "pricing",
eyebrow: "Pricing",
heading: "Fair prices, clear limits",
sub: "Start free. Upgrade when your portfolio grows. Cancel any time.",
tiers: input.plans.map(toPricingTier),
},
{
kind: "final-cta",
heading: "Your first board in two minutes",
sub: "Start free — no credit card, no install.",
cta: { label: "Start free", href: "/signup", variant: "primary" },
},
],
footer: {
brand: { label: "Tasklane" },
tagline: "Ship your roadmap.",
columns: [
{
heading: "Product",
links: [
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
],
},
{
heading: "Company",
links: [
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
],
},
{
heading: "Legal",
links: [
{ label: "Privacy", href: "/legal/privacy" },
{ label: "Imprint", href: "/legal/imprint" },
],
},
],
bottomLeft: "© 2026 Tasklane",
bottomRight: "Made with Kumiko",
},
};
}
/** Convenience: input → final HTML string an app serves as its landing page. */
export function renderLanding(input: LandingInput): string {
return renderApexPage(buildLandingPage(input));
}
/** Sample plan config, as a `tier-engine`-backed app would expose it.
* Used by the test and the screenshot runner. */
export const SAMPLE_PLANS: readonly PlanInfo[] = [
{
key: "free",
name: "Free",
tagline: "For your first project",
monthlyEur: null,
maxProjects: 3,
benefits: ["Live planning board", "Up to 5 collaborators", "CSV export"],
},
{
key: "pro",
name: "Pro",
tagline: "For a growing team",
monthlyEur: 12,
maxProjects: 50,
benefits: ["Everything in Free", "Portfolio view", "Custom fields", "Priority support"],
featured: true,
},
{
key: "enterprise",
name: "Enterprise",
tagline: "For the whole org",
monthlyEur: null,
maxProjects: Number.POSITIVE_INFINITY,
benefits: ["Everything in Pro", "SSO & SCIM", "Dedicated instance", "SLA"],
},
];

📄 On GitHub: samples/recipes/apex-landing/src/feature.ts