Fonio Labs
// CASE STUDY //

Migrating foniolabs.xyz to Sanity + Next.js in 14 days

How I rebuilt a static-export marketing site as a Sanity-driven Next.js 16 app on Vercel, with custom Studio components, GROQ-driven ISR, and a Claude Code + Sanity MCP integration the marketing team actually uses.

Sanity v5Next.js 16VercelGROQClaude Code + MCP14 days

The brief

foniolabs.xyz was a static-exported Next.js site on Hostinger. Every content change required a developer to edit JSX, build, and rsync the output over SSH. The marketing team was effectively locked out.

The brief was twofold. Operationally: migrate to a CMS the marketing team can actually use, without breaking the live site or losing SEO ranking. Strategically: produce a portfolio-grade artifact for an OpenZeppelin Senior Web Developer (Contractor) application — the JD explicitly asked for proven Sanity expertise, a complex CMS migration end-to-end, and interactive elements like live stats. Sprint window: 14 calendar days, ~45 working hours.

By the numbers

From the deployed Sanity manifest + the production Vercel build.

20
Schema types deployed
22
Routes prerendered
4
Custom Studio components
100
SEO score preserved

What I built

Surface-by-surface. Each item is grep-jumpable in the public repo.

Composable content model

3 reusable objects (seo, socialLinks, ctaLink), 9 section blocks (hero, featureGrid, richText, cta, testimonial, logoCloud, embedHtml, contactForm, liveStats), 8 documents. Every page composes from blocks — marketing drops a section in Studio, the renderer picks it up automatically.

Custom Studio components

SlugInput auto-creates a 301 redirect when a published slug is renamed (so editor mistakes never break SEO). VariantChipInput swaps dropdowns for visual chips. SEOPreview renders a live Google SERP snippet. OGImagePreview renders the /api/og card inline.

GROQ + ISR data layer

Single fetch wrapper picks published vs draft client based on draftMode(). Every query carries a cache tag; on Sanity publish, a signed webhook hits /api/revalidate and invalidates the right tags via revalidateTag(). Pages re-render in seconds without a redeploy.

Draft mode + Presentation tool

Editors hit "Preview" in Studio and land on the live site with their unpublished changes rendering inline. A draft-mode banner makes the state visible. Per-doc preview URLs are signed against a read token.

SEO continuity

Sitemap pulled from Sanity slugs at request time. JSON-LD (Organization, Article, Breadcrumb) on every relevant route. Build-time Next.js redirects() reads Sanity redirect docs so the auto-redirects from SlugInput actually fire in production.

Performance pass

urlFor() emits AVIF/WebP via Sanity's CDN. LCP candidates get priority + sizes. Server-component-first rendering keeps the client bundle small. ISR with tag-based revalidation means published content is fresh without serving every request from origin.

Stack

CMS
Sanity v5 (embedded Studio at /studio)
Framework
Next.js 16 App Router · RSC · Turbopack
Language
TypeScript
Styling
Tailwind CSS v4
Hosting
Vercel · region iad1 · git integration
Email / forms
HubSpot Forms (region-aware) · Resend
Analytics
GA4 via @next/third-parties
AI integration
Sanity MCP at mcp.sanity.io · OAuth-scoped roles
// DIFFERENTIATOR //

Claude Code + Sanity MCP

The OpenZeppelin JD specifically asks for AI-native fluency. I shipped a real Claude Code + Sanity MCP integration the marketing team uses to edit content from natural-language prompts.

  • Per-project .mcp.json at the repo root registers the hosted Sanity MCP server (mcp.sanity.io). Anyone who clones gets the integration for free — no per-machine setup.
  • OAuth-based — no token committed to the repo. Each operator's Sanity role gates what they can edit.
  • OPERATING.md walks a non-technical marketer through their first 30 minutes: install, OAuth, prompt cheat-sheet, when NOT to use MCP, troubleshooting.
  • The four canonical prompts (create post · add team member · cross-doc search-and-replace · drop a section onto a page) all work end-to-end. Verified by appending a real liveStatsBlock to the /about page via MCP and watching it render.

What broke (and what I learned)

Full notes in POSTMORTEM.md. The TL;DR:

TODO comments rot silently

Day 5 shipped "visually-rough placeholder" section renderers with a comment promising Day 6 would replace them. Day 6 ended up doing draft mode work; the styling swap was deferred and shipped to prod looking like two different sites. Lesson: a sprint-spanning TODO needs an issue or an acceptance criterion on the original commit, not a comment.

GROQ projection changes need consumer audits

Day 5 added asset-> dereference to the image GROQ projection. The shape changed from { asset: { _ref } } to { asset: { _id, url } }. The team page still checked the old _ref, returning null forever. Hand-written types didn't catch it. Lesson: codegen types from GROQ — groqd or @sanity/codegen — so the compiler enforces the contract.

Schema must be deployed for MCP to ground

Setting up MCP felt complete after OAuth, but list_workspace_schemas returned "No schemas deployed." Claude was unmoored on every prompt referencing field names. The local sanity/schemaTypes/ files don't matter to MCP — only the deployed manifest does. Lesson: npx sanity schema deploy is the silent prerequisite — documented in OPERATING.md so the next person doesn't repeat it.

DNS cutovers: list everything before changing anything

Cutover hit two snags. (1) Adding the Vercel A record without removing the Hostinger one would have load-balanced traffic between two completely different sites. (2) An auto-created IPv6 AAAA record on the apex meant IPv6 clients would have followed Hostinger anyway. Vercel correctly refused to validate until both were resolved. Lesson: dump all records on the apex + www first.

Resources

Everything is in the public repo — including this case study's source under app/(marketing)/case-studies/sanity-migration/page.tsx.

Open to Senior Web Developer roles

Especially when the brief involves headless CMS, content modeling, perf work, or AI-native developer workflows. Let's talk.