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
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 “flexible page builder” 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
Simplest possible. Marketing has to open PRs. Works if marketing is technical or there are very few content changes.
Familiar to marketers. Heavyweight infrastructure. Worth it if the team is already on WordPress and refuses to migrate.
Single source of truth, more cost, more friction for engineering changes. Right answer for content-led organisations.
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.