import { escapeHtml } from "./design.js"; import type { Content, CopyOverride, KnownSection, Tokens } from "./types.js"; const ICONS = [ '', '', '', '', '', '', ]; export const SECTION_LABELS: Record = { hero: "Overview", features: "Features", proof: "Proof", pricing: "Pricing", faq: "FAQ", cta: "Get started", }; interface RenderContext { content: Content; overrides: Map; sections: string[]; } function richOverride(ctx: RenderContext, slug: string): string | null { const override = ctx.overrides.get(slug); return override ? `
\n${override.html .split("\n") .map((line) => ` ${line}`) .join("\n")}\n
` : null; } function renderHero(ctx: RenderContext): string { const { content } = ctx; const custom = richOverride(ctx, "hero"); const panelRows = content.stats .map( (stat) => `
${escapeHtml( stat.label, )}${escapeHtml(stat.value)}
`, ) .join("\n"); return `

${escapeHtml(content.eyebrow)} · ${escapeHtml(content.name)}

${escapeHtml(content.tagline)}

${escapeHtml(content.lead)}

${custom ? `${custom}\n` : ""}

No credit card required · Built for ${escapeHtml(content.audience)}

`; } function renderFeatures(ctx: RenderContext): string { const custom = richOverride(ctx, "features"); const cards = ctx.content.features .map( (feature, index) => `
${ICONS[index % ICONS.length]}

${escapeHtml(feature.title)}

${escapeHtml(feature.body)}

`, ) .join("\n"); return `
Features

What you actually get

Fewer moving parts than the stack it replaces, and every one of them is inspectable.

${custom ? `${custom}\n` : ""}
${cards}
`; } function renderProof(ctx: RenderContext): string { const { content } = ctx; const custom = richOverride(ctx, "proof"); const stats = content.stats .map( (stat) => `

${escapeHtml(stat.value)}

${escapeHtml(stat.label)}

`, ) .join("\n"); return `
Proof

Numbers first, story second

${stats}
${custom ? `${custom}\n` : ""}

“${escapeHtml(content.quote.text)}”

${escapeHtml(content.quote.author)} · ${escapeHtml(content.quote.role)}
`; } function renderPricing(ctx: RenderContext): string { const custom = richOverride(ctx, "pricing"); const tiers = ctx.content.tiers .map( (tier) => ` `, ) .join("\n"); return `
Pricing

Priced so you can start today

Start free. Upgrade when the team does, not when a sales cycle says so.

${custom ? `${custom}\n` : ""}
${tiers}
`; } function renderFaq(ctx: RenderContext): string { const custom = richOverride(ctx, "faq"); const items = ctx.content.faqs .map( (faq) => `
${escapeHtml(faq.q)}

${escapeHtml(faq.a)}

`, ) .join("\n"); return `
FAQ

Questions people actually ask

${custom ? `${custom}\n` : ""}
${items}
`; } function renderCta(ctx: RenderContext): string { const { content } = ctx; const custom = richOverride(ctx, "cta"); return `

${escapeHtml(content.ctaHeading)}

${escapeHtml(content.ctaBody)}

${custom ? `${custom}\n` : ""}

${escapeHtml(content.goal)}

`; } function renderCustomSection(override: CopyOverride): string { return `

${escapeHtml(override.title)}

${override.html .split("\n") .map((line) => ` ${line}`) .join("\n")}
`; } const RENDERERS: Record string> = { hero: renderHero, features: renderFeatures, proof: renderProof, pricing: renderPricing, faq: renderFaq, cta: renderCta, }; export interface NavEntry { id: string; label: string; } export function buildHtml(options: { content: Content; tokens: Tokens; css: string; sections: string[]; overrides: Map; customSections: CopyOverride[]; navEntries: NavEntry[]; }): string { const ctx: RenderContext = { content: options.content, overrides: options.overrides, sections: options.sections, }; const rendered: string[] = []; for (const section of options.sections) { if (section === "cta" && options.customSections.length > 0) { for (const custom of options.customSections) rendered.push(renderCustomSection(custom)); } rendered.push(RENDERERS[section as KnownSection](ctx)); } if (!options.sections.includes("cta")) { for (const custom of options.customSections) rendered.push(renderCustomSection(custom)); } const nav = options.navEntries .map((entry) => `
  • ${escapeHtml(entry.label)}
  • `) .join("\n"); const initials = options.content.name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((word) => word[0]) .join("") .toUpperCase() || "A"; return ` ${escapeHtml(options.content.name)} — ${escapeHtml(options.content.tagline)}
    ${rendered.join("\n\n")}
    `; } /* ------------------------------------------------------------------ */ /* docs */ /* ------------------------------------------------------------------ */