Why My Gatsby to Next JS Migration Failed (And What It Taught Me About AI Workflows)
Robert Marshall

Robert Marshall @robmarshall

About: A React/Next JS developer based in Leeds, UK

Location:
Leeds
Joined:
May 31, 2019

Why My Gatsby to Next JS Migration Failed (And What It Taught Me About AI Workflows)

Publish Date: Aug 14
0 0

I recently tried to publish an article on this blog, and got hit with a number of Cloudflare errors. All stemming from the fact that I had forgotten that I have created the website with Gatsby many years ago.

In past freelance jobs I have migrated, fixed, cleaned up and patched numerous Gatsby websites. It was painful and unforgiving. Not about to do that again… (especially as is a dead framework – development has essentially stopped and the ecosystem is stagnant).

The plan was to migrate the project away. But I am crazy busy with paid work, so a manual migration was not on the cards.

Claude Code is a pretty solid tool in the toolbox for me, so why not see if it can handle the whole thing…

The “Obvious” Approach

My initial instinct was straightforward: write one prompt, outline the migration approach, break the work down into stages, use MD files to handle memory, and hope the sub agents could keep up.

Arguably this is a small website, so there shouldn’t be too much needed… and I am using some pretty powerful tools within Claude Code.

The Tools

Context7 – Pulls in live documentation and context for third-party libraries

Serena – Semantic code search and editing across the entire codebase

Sequential Thinking – Structured reasoning for complex decision making

I wrote a piece about this stack here, if you want more information on how it sits together.

The Prompt

The key was being explicit about the workflow and delegation:

/go
You are looking at an old Gatsby JS project. Your job is to upgrade it to a Next JS App Router project. Some of the components will be able to be left as they are, due to being react. However some will need to be migrated to the Next way of doing things.
The first task is to use 1 subagent to look over the whole project and find a list of files that need to be updated. Once done it should save the list of files into a "migrate.md" file.
Once done, one agent should look over the "migrate.md" file and work out of there is any extra work needed. i.e. util functions, or underlaying Next JS code needed that multiple pages will use. This should be added to the top of the list with brief comment on how they should be used
Once done, these files outlined in at the top of the migrate.md file should be split between 2 sub agents, and should be created.
Once done, rest of the files outlined in the "migrate.md" file should be split between 4-6 sub agents. They should then handle the needed migration. Working through each file one by one.

Enter fullscreen mode Exit fullscreen mode

What Actually Happened

Claude replied with a solid plan in plan mode. It had correctly identified the WordPress headless CMS setup, mapped out the key Gatsby-specific pieces that needed replacing, and understood the dependency chain. The breakdown looked promising – it planned to tackle foundational infrastructure first, then components, then pages.

45 minutes of Claude chuntering away. Divining, and pontificating.

Watching its thinking process and the suggestions it was making (I had it set to automatically execute everything), it seemed like it had its head in the game.

Unfortunately, not so much.

Where It All Went Wrong

Here’s the problem: Gatsby’s complexity isn’t just in its components – it’s in all the custom resolvers, data transformation layers, and vendor-specific abstractions you need to make anything work. When Claude tried to “migrate” this mess, it didn’t simplify it. It recreated the complexity in Next.js.

Claude built a whole collection of systems that sort of did the job, but made everything incredibly heavy. Multiple data fetching layers, complex resolvers that weren’t needed in Next.js, and abstractions on top of abstractions.

In terms of migrating the components and styling – very very happy. This got everything off to the races. But the underlying architecture was a mess.

The Real Problem: I Asked Claude to Solve the Wrong Thing

After about 4 hours of trying to clean up the convoluted migration, I had a realization: I had asked Claude to migrate the wrong thing.

Instead of asking it to “upgrade this Gatsby project to Next.js,” I should have asked it to “help me build a clean Next.js foundation, then help me move specific pieces over.”

The Better Approach: Foundation First

Here’s what I ended up doing, and what I should have done from the start:

Step 1: Clean Slate Foundation

npx create-next-app@latest my-new-blog --typescript --tailwind --eslint --app

Enter fullscreen mode Exit fullscreen mode

Then prompted Claude specifically: “Create a clean Next.js blog architecture with WordPress as a headless CMS. Focus on modern best practices, simple data fetching, and clear separation of concerns.”

Step 2: WordPress Data Layer Foundation

Before touching any components, I needed a solid data layer. I prompted Claude: “Create a lib/wordpress module that handles all WordPress API interactions. Include functions for fetching posts, categories, and individual post data. Set up proper caching with Next.js’s built-in cache, and create a revalidation webhook for when new posts are published.”

This gave me clean, focused utilities like:

  • getAllPosts() with ISR caching
  • getPostBySlug() with proper error handling
  • getCategories() for navigation
  • Webhook endpoint for WordPress to trigger revalidation

Having this foundation meant every subsequent component could use clean, Next.js-native data fetching instead of trying to recreate Gatsby’s complex GraphQL layer.

Step 3: Component Migration

With both the Next.js foundation and WordPress data layer in place, I could then ask Claude to migrate specific components one by one: “Take this Gatsby component and adapt it for Next.js App Router, using our WordPress utilities.”

Step 4: Integration & Testing

Finally: “Wire up these converted components with our WordPress data layer and test the full flow.”

What This Taught Me About AI Workflows

The failure revealed something crucial about working with AI on complex tasks:

AI excels when it has a clear mental model of the target state. When I asked Claude to migrate Gatsby to Next.js, it had to hold both architectures in its head simultaneously while trying to bridge them. When I gave it a clean Next.js project and asked it to add specific functionality, it had a single, clear target to work towards.

Prompt decomposition isn’t just about breaking tasks down – it’s about choosing the right problems to solve. My original prompt was well-structured but fundamentally asked for the wrong thing.

Foundation-first development works better with AI than migration-first. Starting with npx create-next-app gives Claude a proper mental model and established patterns to work with.

The Broader Lesson

This experience changed how I approach complex AI-assisted development tasks. Instead of asking “How do I get from A to B?” I now ask “How do I build B properly, then move the best parts of A over?”

It’s not just about AI either. How many times have we all spent weeks trying to upgrade or migrate legacy code when starting fresh would have been faster and cleaner?

The Outcome

After restarting with the foundation-first approach, the migration took about 4 hours total (including the cleanup time from my first attempt). The resulting codebase is clean, follows Next.js best practices, and actually performs better than the original Gatsby site.

And this is the website you’re reading now.

The key insight: Claude didn’t fail at the migration – I failed at defining the right problem to solve. Sometimes the best migration strategy is to not migrate at all.

Comments 0 total

    Add comment