Skip to content

Migrating a Markdown Blog From Gatsby to Next.js


I recently migrated a Markdown Gatsby blog to Next.js. Although there’s a lot to consider and it takes some time, it’s not as scary as it sounds.

In this post, I will share some lessons you might find useful for facing the same challenge.

There’s an official guide on doing the migration in Next.js docs, which I somehow completely missed it and migrated by myself. You might find it handy.

Swapping the Old With the New

First things first, you need to uninstall Gatsby from your project.

Keep the list of plugins you used in Gatsby somewhere close. You will need to recreate that functionality in Next.js.

Start by removing Gatsby dependencies and scripts section in your package.json.

  "scripts": {
-   "start": "gatsby develop",
-   "build": "gatsby build",
-   "serve": "gatsby serve",
-   "clean": "gatsby clean",
  "dependencies": {
-   "gatsby": "^4.11.1",
-   "gatsby-plugin-sass": "^5.11.1",
-   "gatsby-plugin-sharp": "^4.11.1",
-   "gatsby-plugin-sitemap": "^5.11.1",
-   "gatsby-remark-autolink-headers": "^5.11.1",
-   "gatsby-remark-images": "^6.11.1",
-   "gatsby-remark-prismjs": "^6.11.0",
-   "gatsby-remark-smartypants": "^5.11.0",
-   "gatsby-source-filesystem": "^4.11.1",
-   "gatsby-transformer-remark": "^5.11.1",
    "react": "17.0.2",
    "react-dom": "17.0.2",

Do not remove react and react-dom since Next.js depends on them as well.

Now you can install Next.js.

npm i next

And, add the Next.js scripts.

"scripts": {
+   "dev": "next dev",
+   "build": "next build",
+   "start": "next start",
+   "lint": "next lint"
  "dependencies": {
+   "next": "^12.2.5",
    "react": "17.0.2",
    "react-dom": "17.0.2",

You can run npm run lint to make Next.js set up ESLint, if you don’t have your own configuration already.

If you weren’t using TypeScript before, this is a great opportunity to add it to your project. Next.js has great TypeScript support out of box.

Create a tsconfig.json file in the root of your project.

touch tsconfig.json

Now, run npm run dev and follow the instructions in your terminal.

If something doesn’t work out with the dependencies, you can try deleting your node_modules and package-lock.json and running a fresh npm install.

Cleaning up Files

Now that Next.js is installed, you can clean up some files that Gatsby was using.

Start by deleting .cache and public folders.

Next.js uses the public directory as well, but for static assets. If your site uses images, you will need to put them all inside public and update all image source paths.

Otherwise, you can read a post I’ve written on how to keep images alongside Markdown files.


Update .gitignore by removing .cache and public and adding .next

- .cache
- public
+ .next


Create a file, for example src/site-metadata.json, for your website’s metadata and copy the data from siteMetadata in gatsby-config.js.

  "siteUrl": "",
  "title": "Site Name",
  "description": "Site description."

At this point you can either delete gatsby-config.js keep it somewhere as a reference. I opted for keeping all gatsby-*.js files in a folder named gatsby until I migrated everything to Next.js.

Migrating Pages

Similarly to Gatsby, Next.js also uses pages folder to automatically create pages for you. So you don’t have to change much for pages with simple paths, like pages/about.js. Strip out all the GraphQL and grab your data from the site-metadata.json file your created previously.

The pages that you create programmatically using createPages API in gatsby-node.js need to be completely remade. You will need to use Next.js static site generation (SSG) feature.

In Next.js, to create pages dynamically, you set up a file for a dynamic route like pages/post/[id].js. Then you use getStaticProps and getStaticPaths functions inside [id].js to generate pages for each id route parameter programatically.

You will have to do content sourcing by hand inside getStaticProps function. At first, I used Node’s filesystem module and extracted content from Markdown files by hand. I soon learned about Contentlayer and used that instead. It will save you a lot of time.

You can check out my tutorial on how to create a Next.js blog with Contentlayer.

After migrating pages, you can delete gatsby-node.js.


Although in Next.js you don’t need the templates used in Gatsby, you can turn them into plain components and reuse them.

For example, you can reuse the template for a post page in Next.js, just strip away the GraphQL queries and other Gatsby stuff. Now you can import the component in the pages/posts/[id].js page and use it.

Migrating remark and rehype Plugins

This is the biggest pain when it comes to migrating from Gatsby to Next.js. Gatsby does a good job at abstracting the entire unified ecosystem with its own plugins. You will have to learn about unified, remark and rehype and recreate the Gatsby plugin functionality yourself.

Here’s where Contentlayer can help you again. It has unified running underneath, which allows you to drop in remark and rehype plugins easily.

You can attach remark and rehype plugins in Contentlayer’s config like so.

  remarkPlugins: [remarkSmartypants],
  rehypePlugins: [


If you used gatsby-remark-prismjs, you can use rehype-prism-plus in Next.js instead. But, your syntax highlighting will most likely break, because it uses different class names for styling.

You will need to adjust your PrismJS CSS stylesheet and substitute all the .gatsby-highlight classes with ones that rehype-prism-plus uses.

Another similarity between Gatsby and Next.js, is that they both use their own component for links.

Go through your project and switch all Gatsby’s Link components to next/link instead.

Next.js Link component is a bit different:

  • Use href instead of to prop for URL
  • It has to contain only one child element that must be <a>
  • You can put other elements inside the <a> element
  • Classes for styling go onto the <a> tag
<Link href="/posts">
  <a className="link">Go to posts</a>

Adjusting Styling

In Next.js, global styling imports is done inside pages/_app.js.

Move your imports from gatsby-browser.js to _app.js

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />

export default MyApp

If you use CSS modules, the class names won’t work anymore if they include hyphens.

.css-class {
  /* ... */

In Gatsby css-class was automatically mapped to cssClass, but in Next.js it no longer worked for me.

<h1 className={styles.cssClass}>Welcome</h1>

It threw an error instead.

So, use cssClass format inside CSS Modules.

.cssClass {
  /* ... */

After this, you can delete gatsby-browser.js.

Adding Sass

Next.js has great built-in support for Sass. So it shouldn’t be any trouble to use it instead of plain CSS.

Nothing also stops your from using SCSS with CSS Modules in Next.js,


To create a sitemap in Next.js, you can use the next-sitemap package.

In my case, the sitemap file was changed from sitemap-index.xml to sitemap.xml.


The redirects syntax differs from Gatsby a little bit. You will need to adjust it if you use wildcard redirects.

I moved all my redirects into a redirects.json file.

    "from": "/blog/:slug*",
    "to": "/posts/:slug*"

And here’s how you can set them up in next.config.js.

const redirects = require('./redirects.json');

module.exports = {
  async redirects() {
    return => {
      return {
        source: redirect.from,
        permanent: true,


In Gatsby, your go to tool for setting up SEO is React Helmet. This package is no longer needed in Next.js, you can rewrite your SEO component using next/head.

You can also use next-seo if you want, it has good feedback, but next/head was enough for me.

Keep an eye on your Google Search Console, you might even drop some traffic, but it’s nothing you can’t fix. Mine went back to normal pretty quick.

Since I had some trouble with URL trailing slashes after migrating from Gatsby, I added canonical URLs to my pages. This helped stop duplicate content warnings from Google.

Structured Data

When you use structured data in Next.js, it needs to be set using dangerouslySetInnerHTML, otherwise quotation marks used in the script element are escaped.

  dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}

Use validator on your live site to check if its working.


Next.js comes with a component that optimizes your images out of box. You don’t need any plugins that you used in Gatsby. Just use the Next.js image component.

Like mentioned before, images must be stored in public directory in Next.js. Their source path is relative to the public folder. If you put an image inside public/posts/first-post/image.jpg it’s source path should be /posts/first-post/image.jpg.

Lazy Loading in Markdown

There is no clear go-to way when it comes to lazy loading images inside Markdown files in Next.js. I decided to add MDX support to my project and create my own component that uses next/image.

Here’s a post I’ve written on how to use Next.js Image component inside Markdown.


To summarize the process, here are the steps it takes to migrate from Gatsby to Next.js:

  1. Delete Gatsby dependencies
  2. Install Next.js and optionally TypeScript
  3. Re-implement pages using Next.js SSG
  4. Set up unified ecosystem and add remark and rehype plugins
  5. Update all links
  6. Update global CSS imports
  7. Update sitemap generation and adjust your Google Search Console settings
  8. Update redirects
  9. Get rid of React Helmet and use next/head, pay attention to structured data
  10. Adjust your images

Good luck!