Migrating from Gatsby to Next.js

Recently — and quite frankly I’m shocked this wasn’t a major national news story — I was bored. As I often do when I’m bored, I decided to tinker with this here website. There weren’t any obvious bugs to fix and the design is pretty much sorted, which unfortunately left very little to tinker with. All hope was not lost, as I have been curious about Next.js’s static site generation since it debuted with version 9.3 of the framework. This seemed an opportune time to test it out independent of content, design, or site structure. This also meant moving away from Gatsby, which I have been using as the basis for this site for the last 3 years.

Gatsby vs. Next.js

If you’re unfamiliar, Gatsby and Next.js are frameworks for developing highly-performant React-based websites. They share some things in common, but differ in core philosophy: Gatsby is a static site generator first and foremost, while Next.js is a more general server-rendered framework for React applications. While both offer the ability to generate static pages, only Next.js has the ability to generate server-rendered pages on the fly (though, contrary to popular belief, Gatsby sites can have client-only routes). The other main difference is their approach to data-fetching: in Gatsby, all data is accessed via an internal GraphQL API while Next.js is completely agnostic as to how the data finds its way to your components.

What’s Wrong With Gatsby?

Nothing.

Well, almost nothing. As Jared Palmer expertly points out, the unified data graph approach of Gatsby often introduces more complication than is necessary for small static sites such as this. (You might find yourself asking “isn’t any React framework more complication than is necessary for small static sites such as this?” And to that I say yes. But this site is just as much a portfolio piece as a practical bit of code and also do you hate fun?)

But ultimately this was an excuse to dip my toes in the Next.js waters. I only fully dived in when it turned out the temperature was to my liking.

Making the Switch

The Next.js docs have a handy migration guide, which seemed as good a place to start as any. Following these steps got me most of the way there, but I needed to do some work to get the site across the finish line. A bulk of that work was replicating functionality that plugins were providing in the Gatsby version of the site. There are Next.js plugins, but for the most part extra functionality is left up to the user. Here’s some of the notables:

MDX

Like I said, Next.js does have plugins. One of them is for MDX. Following the instructions for the @next/mdx plugin got the files compiling. Since Next.js doesn’t handle frontmatter directly, I had to convert the frontmatter for each file to an object and manually export the template with the data. I could have used getStaticPaths for this, but rendering the files in place felt the best.

Dates

An underrated aspect of Gatsby is the ability to format dates at build time without installing any packages or doing any date manipulation on the frontend. Luckily, libraries like date-fns make manipulating dates in JavaScript fairly easy (standard library when?). Combining parseISO with format got me date strings for my blog post headers without any timezone wonkiness.

Theme UI

Setting up was Theme UI was actually the first thing I did, since literally every component on the site would look like hot garbage without the ThemeProvider. Because I would need to wrap every page of the site in the provider, I created a custom App to do so. This also allowed me to wrap every page with my Layout component (à la Gatsby v1), meaning I didn’t have to manually include it in every page.

import * as React from 'react'
import { ThemeProvider } from 'theme-ui'
import theme from '../constants/theme'
import components from '../components/MDXComponents'
import Layout from '../components/Layout'
const App = ({ Component, pageProps }) => (
<ThemeProvider theme={theme} components={components}>
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
)
export default App

Color Mode Flash

With just the ThemeProvider, there will be a flash of the default color mode on initial page load, which can be jarring for users who are using an alternative color mode. Luckily, Theme UI is aware of the problem, and offers a handy InitializeColorMode component to solve for it. In Next.js, this is used in a custom Document:

import * as React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
import { InitializeColorMode } from 'theme-ui'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<InitializeColorMode />
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

The Link section of the migration guide doesn’t quite paint the full picture. There is one significant difference between Gatsby and Next.js’s Link components: the latter isn’t always an <a> element. If the component’s child isn’t a text node or itself an <a>, Link just adds an onClick handler to the child and calls it a day. This means that I couldn’t use Theme UI’s as prop to copy the functionality, and that I needed to add a passHref prop to each Link to force Next.js to add an href the <a> rendered by Theme UI’s Link. I didn’t want to do this for every link on the site, so I made a custom Link component to combine the two:

import * as React from 'react'
import { default as NextLink } from 'next/link'
import { Link as ThemeUILink } from 'theme-ui'
const Link = ({ href, ...props }) => (
<NextLink href={href} passHref>
<ThemeUILink {...props} />
</NextLink>
)
export default Link

RSS Feed

I’m one of the dozen or so people that continued to use an RSS app after Google decided to off Google Reader, murder-style. Getting a working RSS feed was by far the most challenging part of this process. I nearly gave up on the conversion altogether because of it — and I’m still not very happy with where I ended up with it. After failing to get my MDX to compile in a custom node script, I brute forced it and extracted the meta object (frontmatter) from the MDX files by way of regex. This means my feed no longer contains the full contents of the article, which is a bummer. Maybe I’m missing something obvious here. Maybe this is a solved problem. But I wasn’t able to find an example of a statically-rendered Next.js site using MDX directly that had an RSS feed, and I didn’t want to be held up on this (I estimate approximately negative 3 people are subscribed to the feed here).

Luckily, the rss package made actually generating the feed super easy once I had the data. I added a postbuild script to my package.json that generates a sitemap and an RSS feed every time the site is built.

Update (June 2021)

Turns out there are a few solutions to handle frontmatter in MDX with Next.js, and I explored them. I used next-mdx-enhanced for a while, and had mdx-bundler working locally (I never tried next-mdx-remote because it was so similar to mdx-bundler, and I preferred the latter’s API). Ultimately each had issues I either couldn’t or didn’t want to deal with, so I wound up going back to the @next/mdx. I refined my regex-fu and used a very completely 100% safe eval to extract the metadata necessary to create a half decent RSS feed.

Preact

Gatsby has a really handy plugin to use Preact in production, reducing your bundle size by a fair amount. If your Gatsby site doesn’t need to support IE10, I highly recommend using it. While there isn’t a plug-and-play option for Next.js, we can use npm aliases to replace references to React with the preact/compat package. Combined with some webpack wizardry, we can reduce the size of our builds a good amount. Here’s the official example that I definitely didn’t just copy & paste from.

Update (February 2021)

I ultimately wound up migrating back to React proper — I didn’t want to deal with the headache of managing the custom chunk splitting that using Preact required. However, the Preact team has since extracted that behavior into a standalone plugin. Maybe I’ll waffle on this again, but for now its nice not having to double-check compatability for every new Next.js release.

Should You Switch?

While I’m happy with the outcome of this exercise, I can’t possibly answer that. If your Gatsby site is working, and working for you, I would err on the side of “no”. This is doubly true if your site has multiple sources of data: working with multiple source APIs is Gatsby’s true strength (or at least it’s where the unified graph approach to site building shines brightest). That said, I was clearly happy enough with the early results of the exercise to put further time into getting the site into shipping state. Here are some reasons why:

Fewer Dependencies

I’m pretty adamant about keeping my dependencies up to date. Moving on from the plugin-centric Gatsby approach to the userland Next.js approach meant I got to remove a lot of those dependencies. Like, a lot a lot. Like, my package-lock.json lost 15 thousand lines of code a lot. Some of this was surely due to my having too many plugins (this site has exactly one image on it at time of writing, gatsby-image was probably overkill). But a lot of it has to do with Next.js managing internally a lot of things that Gatsby leaves to plugins.

Simpler Data Flow

Doing things the Gatsby way means pulling your data out of the unified GraphQL API. As mentioned earlier, that’s great when you have lots of data sources, but less so when you’re working on a tiny blog like this. Plus, since getStaticProps runs at build time, you can move some costlier work there to boost production performance. You can do similar things with Gatsby, of course — but not as simply.

Only As Static As You Want

If I ever wanted to create a truly dynamic page, using Next.js allows me to do that. For example, the homepage currently fetches new stats on mount, and updates them when the request completes. Moving that work to a server could reduce page shift, while continuing to ensure the stats are always accurate (doing so would require me to change hosting providers, but that’s a can of worms for another day).

Update (October 2020)

A mere [checks notes] week after I initially made this migration, I did wind up switching the way the homepage fetches the stat data. It’s not quite fully server-rendered, however; I’m using something Next.js calls “incremental static regeneration” (ISR). ISR is very simple conceptually: it allows you to tell Next.js how often you expect your static props to change, and re-calculates them at most that often. It is very much a perfect middle-ground between statically-generated and server-rendered content. In my case, the homepage will request new stats once every 15 minutes.

What Did We Learn?

I’ll do a lot of tinkering when I’m bored.