If you're trying to migrate Gatsby to Next.js and you've landed here after a failed npm install or a Node version conflict, you're not alone. Gatsby's plugin ecosystem quietly rotted over the past two years, and most teams are now sitting on sites they can't upgrade, can't deploy cleanly, and can't hand to a junior developer without a full briefing on which workarounds hold the whole thing together.
This post covers the migration path I've used on real Gatsby + Sanity projects — what breaks, what carries over, and how to protect your URLs and your editors when you make the jump.
The symptoms that tell you it's time
The clients who reach me at this point usually describe the same cluster of problems:
- Node 18 or 20 breaks the build.
gatsby-plugin-sharp,gatsby-source-sanity, or something deeper in the Gatsby bootstrap throws on modern Node. The workaround is pinning Node 16 in.nvmrcand hoping the hosting platform still supports it. - Plugin PRs go unanswered. Gatsby is technically "in maintenance mode" but in practice that means bugs sit open and the people who maintained the plugins have moved on.
- Build times are painful. Cold builds on a mid-size Sanity-backed Gatsby site often run 8–15 minutes because Gatsby pulls every document into a local GraphQL layer at build time.
- Incremental builds broke silently. The promise of Gatsby's incremental cache worked sometimes, in some CI environments, and now it mostly doesn't, and nobody can tell you why.
- The Studio is fine but the front end is the problem. Sanity v3 Studio is perfectly healthy — it's the Gatsby front end that's the liability.
The cost of staying is real and it compounds. Every month you stay on a Node 16 pin is a month further from being able to adopt anything new. Every plugin update you skip because it might break the build is technical debt that lands on whoever inherits the codebase next.
What a Gatsby + Sanity site actually looks like under the hood
Before I get into the migration, it's worth saying what you're actually dealing with. A typical Gatsby + Sanity site has:
-
gatsby-source-sanitypulling documents into Gatsby's GraphQL data layer - Page creation in
gatsby-node.jsusingcreatePageagainst GraphQL queries - Image handling through
gatsby-plugin-imagewith Sanity's image source - A mix of static pages and dynamically generated routes under a handful of URL patterns (
/blog/:slug,/case-studies/:slug, etc.)
The good news: Sanity itself is portable. Your schemas, your content, your Studio — none of that is coupled to Gatsby. You're migrating the rendering layer, not the CMS.
The migration path I actually use
Phase 1: Audit before you touch anything (week 1)
I start by crawling the live site with a tool like wget --spider or Screaming Frog to get a complete URL inventory. Every path that exists in production goes into a spreadsheet. This becomes the URL parity checklist and the redirect map if anything changes shape.
At the same time, I audit the Gatsby GraphQL queries in each page template. These become the GROQ queries in the Next.js version. The data requirements are the same — the query language changes.
I also note which Gatsby plugins are doing real work versus which ones are handling things Next.js handles natively (image optimisation, font loading, sitemap). That shortens the dependency list considerably.
Phase 2: Build the Next.js shell in parallel (weeks 2–3)
I create the Next.js App Router project in a /next subdirectory of the same repo, or in a separate branch — never replacing the Gatsby project until the new one is verified. The client's Sanity project ID and dataset go straight into .env.local. The Sanity client setup is identical to any other Next.js + Sanity project.
The route structure maps like this:
// Gatsby: gatsby-node.js createPage for /blog/:slug
// Next.js App Router equivalent:
// app/blog/[slug]/page.tsx
// app/blog/[slug]/page.tsx
import { client } from '@/sanity/lib/client'
import { notFound } from 'next/navigation'
type Props = { params: Promise<{ slug: string }> }
export async function generateStaticParams() {
const slugs = await client.fetch<{ slug: string }[]>(
`*[_type == "post" && defined(slug.current)]{ "slug": slug.current }`
)
return slugs.map((s) => ({ slug: s.slug }))
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await client.fetch(
`*[_type == "post" && slug.current == $slug][0]{
title, body, publishedAt,
mainImage{ asset->{ url, metadata { dimensions, lqip } }, hotspot, crop }
}`,
{ slug }
)
if (!post) notFound()
// render
}
This pattern replaces the entire gatsby-node.js createPage loop. generateStaticParams at build time, GROQ instead of GraphQL, notFound() instead of a 404 redirect in the Gatsby config.
Phase 3: URL parity and redirects (week 3)
Gatsby's default trailing-slash behaviour differs from Next.js. If your Gatsby site served /about/ and Next.js serves /about, search engines see them as different URLs and you'll haemorrhage rankings.
In next.config.ts, set trailingSlash: true if your existing URLs use trailing slashes, or write explicit redirects for any paths that genuinely change shape during the migration:
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
trailingSlash: true,
async redirects() {
return [
// example: Gatsby used /news/:slug, Next.js uses /blog/:slug
{
source: '/news/:slug',
destination: '/blog/:slug',
permanent: true,
},
]
},
}
export default config
Before launch I run the URL inventory from phase 1 against the new site using a script that checks every path and logs non-200 responses. No path should 404 that didn't 404 before.
Phase 4: ISR instead of long builds (week 4)
This is where the biggest operational win lands. Rather than rebuilding every page on every deploy, I set revalidate on the data fetches that change frequently, and use Sanity webhooks to trigger on-demand revalidation for specific documents when editors publish. Build times drop from 12 minutes to under 2 minutes because the static generation at deploy time only handles the shell — content updates revalidate in the background.
Phase 5: Staging review and handoff
I deploy the Next.js build to a Vercel preview URL, share it with the client for a two-week parallel run, and then do the DNS cutover once they've signed off. The Sanity Studio URL doesn't change. The editors notice nothing except that the front end is faster.
Realistic timeline
For a Gatsby + Sanity site with 5–10 content types and 200–500 pages, the migration runs 4–6 weeks: one week of audit, two weeks of build, one week of URL/redirect verification, one week of staging review. More complex sites with e-commerce, internationalisation, or heavy custom Gatsby plugins add 2–4 weeks.
What to ask a developer before you hire for this
Ask them how they handle URL parity — if they say "redirects" without mentioning a pre-launch URL audit and automated checking, that's a gap. Ask whether they'll run the Gatsby and Next.js sites in parallel before cutover, or go straight to a swap. Ask how they'll test that every editor workflow still functions post-migration. The Sanity Studio is untouched but webhooks, preview links, and any custom document actions need verifying against the new front end.
This is the kind of migration I run for clients — Gatsby to Next.js App Router, Sanity intact, URL parity confirmed before the DNS moves. If your Gatsby site is pinned to Node 16 and your builds are breaking, get in touch.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.