;
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)}”
`;
}
function renderPricing(ctx: RenderContext): string {
const custom = richOverride(ctx, "pricing");
const tiers = ctx.content.tiers
.map(
(tier) => `
${tier.featured ? 'Most popular' : ""}
${escapeHtml(tier.name)}
${escapeHtml(tier.price)}${escapeHtml(
tier.cadence,
)}
${escapeHtml(tier.blurb)}
${tier.items.map((item) => ` - ${escapeHtml(item)}
`).join("\n")}
${escapeHtml(
tier.featured ? ctx.content.goal : `Choose ${tier.name}`,
)}
`,
)
.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)}
`,
)
.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 ` `;
}
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)}
Skip to content
${rendered.join("\n\n")}
`;
}
/* ------------------------------------------------------------------ */
/* docs */
/* ------------------------------------------------------------------ */