All blueprints
Marketing Site

Headless CMS Marketing Site. Fast, editable, sane.

A pragmatic pattern for marketing sites where engineers want content as code, marketers want a CMS, and nobody wants to wait two minutes for a deploy. Content as MDX in the repo by default, optional Sanity for the pages marketing edits weekly, ISR for instant cache invalidation.

Components

Next.js (App Router)
Static-first rendering with selective ISR for editable pages.
MDX (default)
Most content lives as MDX in the repo. Engineers edit, version control, PR review.
Sanity (optional)
For pages marketing edits weekly without engineering involvement. Hydrated server-side.
next-sanity + GROQ
Typed query layer for Sanity content. Live preview support.
Vercel ISR + on-demand revalidation
Static performance with instant content updates via webhook.
Image optimisation
Next/Image with Sanity asset URLs or local imports.
Tailwind + design system
Consistent typography and spacing across MDX and CMS content.

When to use this

  • Marketing site for a startup or solo brand, with mixed engineer + marketer ownership
  • Some content barely changes (about, pricing structure) — content as code is right
  • Some content changes weekly (case studies, blog posts) — CMS is right
  • You want SEO-grade static performance with the option of instant updates

When not to use this

  • ×Pure documentation site — use a doc framework like Mintlify or Docusaurus
  • ×Editorial-heavy publication with daily content from many writers — use a proper CMS like Contentful
  • ×No marketing involvement at all — keep it MDX and skip the CMS layer
  • ×Site content is dynamic per-user — that is an app, not a marketing site

The split

Decide which pages live in MDX and which live in Sanity, by ownership. If a marketer owns the page and edits it more than once a quarter, it lives in Sanity. Otherwise it stays in MDX.

In practice this usually means: home page, key landing pages, case studies, and blog posts in Sanity. About, pricing structure, FAQs, legal pages in MDX. The exact split is per project.

MDX setup

MDX content lives under `content/<section>/<slug>.mdx` and is statically rendered at build time. Frontmatter supplies metadata. A small typed loader exposes content to pages.

// lib/content.ts
import { readFile, readdir } from 'node:fs/promises'
import matter from 'gray-matter'
import { compileMDX } from 'next-mdx-remote/rsc'

export async function loadDoc(section: string, slug: string) {
  const raw = await readFile(`content/${section}/${slug}.mdx`, 'utf8')
  const { data, content } = matter(raw)
  const { content: mdx } = await compileMDX({ source: content })
  return { meta: data, body: mdx }
}

Sanity setup

Sanity is configured with a tight schema — no &ldquo;flexible page builder&rdquo; nonsense. Every editable page has a defined schema with a fixed set of fields. This keeps editing fast for marketers and prevents the layout from drifting.

// sanity/schemas/case-study.ts
export const caseStudy = {
  name: 'caseStudy',
  type: 'document',
  fields: [
    { name: 'title', type: 'string', validation: r => r.required() },
    { name: 'slug', type: 'slug', options: { source: 'title' } },
    { name: 'industry', type: 'string' },
    { name: 'summary', type: 'text', rows: 3 },
    { name: 'body', type: 'array', of: [{ type: 'block' }] },
    { name: 'metrics', type: 'array', of: [{ type: 'metric' }] },
  ],
}

On-demand revalidation

Sanity webhook fires on publish. A Next.js route revalidates the affected paths. Marketing publishes; the site reflects the change inside seconds.

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'

export async function POST(req: Request) {
  const secret = req.headers.get('x-sanity-secret')
  if (secret !== process.env.SANITY_REVALIDATE_SECRET) {
    return new Response('unauthorised', { status: 401 })
  }
  const { _type, slug } = await req.json()
  if (_type === 'caseStudy') revalidatePath(`/work/${slug}`)
  revalidateTag(_type)
  return Response.json({ ok: true })
}

Performance and SEO

  • Static rendering by default; ISR only on Sanity-backed pages
  • next/image for every image, with width/height to prevent layout shift
  • Open Graph image generation via next/og for blog and case-study pages
  • Sitemap and robots.txt generated from a single source of truth
  • Lighthouse 100 / 100 / 100 / 100 should be the baseline, not the goal

Alternatives I considered

Pure MDX (no CMS)

Simplest possible. Marketing has to open PRs. Works if marketing is technical or there are very few content changes.

WordPress headless

Familiar to marketers. Heavyweight infrastructure. Worth it if the team is already on WordPress and refuses to migrate.

Sanity for everything

Single source of truth, more cost, more friction for engineering changes. Right answer for content-led organisations.

Notion as CMS

Beloved by marketers. API is slow, schema is rigid, hard to model anything beyond simple posts. Charming for very small teams.

Want me to build this for you?

Blueprints are how I think. If your problem fits one of these, we are already most of the way to a quote.