launch-quickly

The marketing surface

One config file holds every word on the public site, thirteen blocks render it, two layouts arrange it, and the SEO helpers keep the structured data agreeing with the page.

The signed-in shell has been config-driven since src/config/nav.ts — one declaration feeds the sidebar, the breadcrumbs and the generator. The landing page was the opposite: six components with the copy written inside the JSX.

So changing the headline meant editing a component, adding a pricing tier meant rewriting a card, and three products built on this template shipped the same page in the same order. Not because the people building them liked it — because editing six components is work, and the config file was right there for the sidebar.

src/config/marketing.ts

Every word the public site says: identity, nav links, hero, features, stack, stats, testimonials, pricing tiers, FAQ, the closing call to action, footer groups. Replace every string in that file and you have a different landing page without opening a component.

The blocks in src/components/blocks/ render it. Section and SectionHeader are the shared frame, so a block you add lines up with the ones already there — three different container widths down one page is the thing nobody can name and everybody reads as cheap.

The blocks

Navigation (one call to action, and a real mobile menu — the old header just hid its links below md, so a phone visitor could reach nothing), Hero in a stacked or split variant, TechCarousel, Features, Stats, Testimonials, Pricing, Faq, CtaBand, ScreenshotFrame, Footer.

Two of them are new in the sense that matters: Pricing renders one card per tier in the config, where before it was a single hardcoded $29 card, and Faq exists at all. The FAQ uses native <details> rather than an accordion component, because an accordion unmounts its answers while closed — invisible to a crawler, invisible to Cmd-F, and gone entirely without JavaScript. This is the rare case where the primitive beats the component.

Blocks that render nothing are doing their job

Stats and Testimonials return null while their config arrays are empty, and they ship empty. The placeholders in the config are literal em dashes, not plausible figures.

That is deliberate. "10,000+ developers" on a product with eleven customers is a claim you cannot walk back, and three invented testimonials are the most common lie on a landing page and the easiest to catch — the first prospect who searches a name and finds no such person has learned something no feature list undoes. An empty section is honest. Fill it when you have a real quote from a real customer, with their real role.

The same rule governs the stack strip: an earlier version of this template advertised Supabase, which it does not use.

Two layouts, because section order is an argument

landing.tsx composes the same blocks two ways. product-first explains the product, then prices it, then answers objections — the shape when the idea itself is the news. proof-first leads with a split hero, then numbers, then quotes, then the explanation — it argues from evidence, and it needs evidence to exist.

Neither is the good one. Switch with a prop in src/app/(marketing)/page.tsx, then edit it; they are starting points, not a menu you are stuck inside.

SEO

src/lib/seo.ts. The root layout already sets metadataBase, the title template and the site-wide OpenGraph defaults, so a page needs one call:

export const metadata = pageMetadata({
  title: 'Pricing',
  description: 'What it costs and why.',
  path: '/pricing',
});

That gives OpenGraph its own title and description and sets a canonical URL. Both are things nobody does by hand: without the first, every share of every page shows the homepage blurb; without the second, a page reachable at two URLs splits its own ranking.

organizationJsonLd(), productJsonLd() and faqJsonLd() build schema.org objects from the same config the blocks render, so the marked-up price cannot drift from the visible one — a mismatch there gets rich results suppressed, and the copy is the half people remember to update. An e2e asserts the two agree.

sitemap.ts, robots.ts and a generated OpenGraph image ship as well. One thing to remember when you add a public page: src/proxy.ts lists public paths, so a new route is private by default — for a crawler as much as for a person. /docs shipped as a 307 to sign-in once, through a fully green build. Curl it.

On this page