launch-quickly
Patterns

Marketing and SEO

The public site — marketing.ts as the single copy declaration, which block to use where, the two landing layouts, and the SEO helpers (page metadata, canonicals, JSON-LD, sitemap). Use when editing the landing page, adding a public page, or changing anything a crawler sees.

Generated from .claude/skills/marketing-and-seo/SKILL.md, which ships in every project created from this template. Your agent loads it on demand; this page is the same text. Edit the skill, not this page.

Everything the public site says lives in src/config/marketing.ts. The blocks in src/components/blocks/ render it. That split is the whole convention: change words in the config, change arrangement in a block, and never put a sentence inside JSX.

It exists for the same reason nav.ts does. When the copy was hardcoded across six components, changing the headline meant editing a component — so nobody did, and three products built on this template shipped the same landing page.

The blocks, and when to use each

blockjob
Navigationheader. ONE call to action; sign-in is a quiet link.
Herovariant="stacked" before you have a screenshot, variant="split" after. capture picks waitlist vs sign-up.
TechCarouselthe stack strip. Only what you actually run.
Featuresfour claims that can be demonstrated, not nine capabilities.
Statsnumbers you can defend. Renders nothing while empty.
Testimonialsreal quotes from real people. Renders nothing while empty.
Pricingone card per tier in config; exactly one featured.
Faqobjections, not features.
CtaBandthe last decision point before the footer.
ScreenshotFramea shot of the real product, framed like a window.
Footerlink groups from config.

Section and SectionHeader are the shared frame. Use them for any block you add, so the new section lines up with the others down the page — three different container widths is the thing nobody can name but everybody reads as cheap.

Two layouts, because section order is a positioning decision

src/components/blocks/landing.tsx composes the same blocks two ways:

  • product-first — hero, stack, features, pricing, FAQ. The shape when the idea itself is the news.
  • proof-first — split hero, stack, stats, testimonials, features, pricing, FAQ. Argues from evidence; needs evidence to exist.

Switch with one prop in src/app/(marketing)/page.tsx. Then edit it — these are starting points, not a menu you are stuck inside.

Blocks that render nothing are doing their job

Stats and Testimonials return null while their config arrays are empty, and they ship empty. Filling them with plausible-looking numbers and quotes is the most common lie on a landing page and the easiest to catch: the first prospect who searches an invented name has learned something about you that no feature list undoes. An empty section is honest. Delete the block if you never intend to fill it.

The same rule applies to the stack strip and the feature list — the earlier version of this template advertised Supabase, which it does not use.

SEO

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

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. Both are things nobody remembers by hand: miss the first and every share of every page shows the homepage blurb; miss the second and a page reachable at two URLs splits its own ranking.

noIndex: true for pages that exist but should not be found — unlinked, internal, gated.

Structured data

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.

Render them in a <script type="application/ld+json">. Only include faqJsonLd() on a page that actually shows the questions — describing content a visitor cannot see is a guideline violation, and the penalty lands on the whole site rather than the page.

The rest of the crawler surface

  • src/app/sitemap.ts — a flat list of PUBLIC routes. Add yours when you add a public page; nothing derives it from the route tree, on purpose.
  • src/app/robots.ts — disallows /dashboard/ and /api/.
  • src/app/opengraph-image.tsx — the generated share image.
  • New routes are PRIVATE by default. src/proxy.ts lists public paths, so a public page that is not in PUBLIC_PATHS returns a 307 to sign-in — for a crawler as well as a person. /docs shipped that way once, through a fully green build. Curl a new public route; do not assume.

Copy rules worth keeping

  • One call to action per screen. Two competing buttons is none.
  • The headline says what the product does; the sentence under it says what the reader gets. Neither says what it is built with.
  • Name who it is NOT for. Almost nobody does, and it is the single most credible thing you can put on a landing page.
  • Use text-muted-foreground for secondary text, never opacity-*. Opacity fades toward the background and lands somewhere different in dark mode than every other muted line on the page.

On this page