An Idiosyncratic Blog

Goodbye Gatsby, Hello Next

Published on
17 minutes read

I've been maintaining this site for several years. I initially started with AngularJS (yes the version of Angular that we want to forget), then moved to Angular (the TS one). My main site and blog were two different apps living in different locations.

2019 was the year when I moved to Gatsby and decided to merge my main site and blog. Static Site generators were the rage, and I wanted to hop on that bandwagon. Gatsby was the preferred framework of choice because it gave a lot of options out-of-the-box with minimal configuration. Coming over from a CMS tool like Ghost, it made a ton of sense to me as I had more control over the architecture of the app. I could decide what format my content would live in, and what framework I would be using.

I had just started with React in 2018, and it seemed like an obvious choice to use Gatsby. So I customized the look and feel of my site, migrated all my content over from Ghost to plain Markdown files, and deployed to Firebase Hosting. Providers like Netlify were gaining traction, but since I was already hosting my main site on Firebase (the Angular one), I just had to upload the Gatsby build files to Firebase, and it still just worked. Because at the end of the day, everything were just static files.

Table of Contents

The Great Gatsby

When I first started with Gatsby, I was really blown away with the capabilities which were available without any configurations. Moving from a single page app to a static site, I could feel the difference between each page load. Since the HTML was generated at build time, the pages were snappy. There was no dance of App Shell to Content load to rehydration. Everything was snappy.

Gatsby has a vast and extensive community of contributors and plugins. There's a Gatsby plugin for everything. (which later on caused more head-aches for me. More on this later.). And the baked in GraphQL support meant I didn't have to worry about where to keep my content or images. The plugins mostly took care of it.

Why I loved Gatsby?

  • Great preset out-of-the-box like image optimizations, preloading of pages, code splitting and accessibility.
  • No lock-in with any kind of data source. Be it Markdown, a git repo, or a CMS, with an extensive plugin ecosystem, API support for adding any data to Gatsby’s GraphQL layer meant I did not have to care about where the content lived. And as an added bonus, Gatsby comes with a GraphQL playground which runs along the dev server.
  • Gatsby has an extensive open source ecosystem. The community has over 2,000 plugins, which means there's usually a ready-made tool to accomplish anything.
  • Gatsby’s documentation is clear, comprehensive, and provides context not just on the internals of Gatsby work, but on everything from setting up the development environment to building custom plugins.
  • CSS Modules are supported by default. No configurations needed. I've been using SASS for a long time and wanted to try something new. CSS Modules caught my eye, and Gatsby made it easy to use.
  • Unparalleled markdown support with remark plugin ecosystem. I could enable features like auto link headers, provide custom component for an element, use front matter to populate SEO meta tags and a lot more by just including a couple of plugins.

It's not you, it's me.

It was a great honeymoon period, and I was thrilled with the performance of my site. But, like in any relationship, problems started.

  • Developer Experience started to go downhill. While Gatsby initially gave better performance, as the amount of pages/content and components increased, the performance started taking a hit. HMR would be painfully slow whenever I updated any content, because Gatsby had to re-run all the queries. I used to face random cryptic errors and the only way to fix these were to clear the cache and restart dev server, which took its sweet time.
  • All that magic of GraphQL started to fade. Gatsby has a great default configuration and the presets are the recommended way to get started with Gatsby. However, that's where the magic ends. Trying to customize the GraphQL API required a lot of research, and it's tricky to figure out what goes where. Every single data source you use needs a GraphQL wrapper. Which meant plugins (and sometimes, plugins for plugins).
  • Plugins fatigue is a real thing. If you've used Gatsby, you know that there's a plugin for everything. The official plugins were great. But there were some plugins which were of not so high-quality. And the more plugin I needed, performance took a greater hit because Gatsby has to process all the things the plugin does and then some.

Here is an excerpt from my package.json with the current list of plugins:

    "gatsby-plugin-feed": "^3.6.0",
    "gatsby-plugin-google-gtag": "^3.9.0",
    "gatsby-plugin-image": "^1.6.0",
    "gatsby-plugin-manifest": "^3.6.0",
    "gatsby-plugin-nprogress": "^3.6.0",
    "gatsby-plugin-offline": "^4.6.0",
    "gatsby-plugin-react-helmet": "^4.6.0",
    "gatsby-plugin-sass": "^4.6.0",
    "gatsby-plugin-sharp": "^3.6.0",
    "gatsby-remark-autolink-headers": "^4.3.0",
    "gatsby-remark-copy-linked-files": "^4.3.0",
    "gatsby-remark-embed-snippet": "^6.3.0",
    "gatsby-remark-images": "^5.3.0",
    "gatsby-remark-prismjs": "^5.3.0",
    "gatsby-remark-responsive-iframe": "^4.3.0",
    "gatsby-remark-smartypants": "^4.3.0",
    "gatsby-source-filesystem": "^3.6.0",
    "gatsby-source-git": "^1.1.0",
    "gatsby-source-graphql": "^3.6.0",
    "gatsby-transformer-json": "^3.6.0",
    "gatsby-transformer-remark": "^4.3.0",
    "gatsby-transformer-sharp": "^3.6.0",
  • A lot of plugins depended on specific versions of each other. gatsby-plugin-sharp and gatsby-plugin-image are some image optimization plugins, and they needed to be upgraded together. I always had to make sure to check what order the plugins need to be called in order for them to work as expected. Honestly, this was a real chore.

Gatsby is a great framework. I'm not going to deny that. They have world-class documentation and Gatsby tutorials are the gold standard for open source documentation. Plugins ranging from sourcing data, SEO, and AMP pages it's all there. To me, GraphQL was an extra layer of complexity that felt unnecessary. Reading from file system should not involve using complex GraphQL queries. The more I iterated on my site, the more issues I encountered. For simple things like loading images I had to figure out a lot of GraphQL queries.

I was dedicating hours maintaining, fixing, and figuring out the exact order of the plugins, just to get things working. Over time, I grew tired of dealing with Gatsby's shortcomings and started spending a lot of time building simpler implementation, making using Gatsby's offerings less and less justifiable. I had a poor understanding of Gatsby's GraphQL layer, and it took a lot of effort and patience to read through every example provided in the docs. Between getting frustrated with Gatsby's complex technical choices, and having a poor developer experience, I decided it's time to move on.

What's Next?

I decided to use Next.js. It's a React framework, which meant most if not all, of my code would still work. The routing setup is extremely easy. It supports generating pure static sites. And it's a trending framework with tremendous community support.

Architecture

When I decided to revamp the site, I had a few main goals:

  • Use Markdown, more specifically MDX, for content.
  • Re-use most existing React components.
  • Continue to deploy on Firebase Hosting.
  • Use Tailwind CSS, because it's hot right now.
  • Dark Mode and Light Mode.
  • A more modern design.
  • Static site as a default.
  • Probably a decent SEO.

Inspiration

I spent some time, researching and looking at different blogs built using Next.js. I came across Tailwind Next.js started template. The template proved to be a great starting point for me to work with. I was able to use most of the MDX parsing code with minimal modifications. The template uses a custom version of rehype-prism-plus plugin, which I forked and made changes to suit my requirement. (I did contribute back, as a model open-source citizen should!)

Tailwind 💨

I've been working with CSS for over a decade now, building and accumulating my own set of CSS utilities which over the years, grew to accommodate responsive designs and multiple theme variants. Most of which looked like this:

<button class="btn-primary btn-primary__hover"></button>
<button class="btn-primary btn-primary_shadow"></button>
<button class="btn-secondary btn-secondary_text-bold"></button>

<div class="container container-flex container-flex_row">
  <div class="content content-flex_center"></div>
</div>

<img class="img-rounded img_shadow" src="..." />

I was a preacher of BEM class notations and extensively advocated for using SASS to manage complex stylesheets.

When building the last iteration of my site with Gatsby, I used CSS modules. It enables importing CSS classes as JavaScript objects. It was great because it allowed class names and selectors to be scoped during the build process. The classes were dynamically generated, unique to each component and solved the issue of styles bleeding into another component when incorrectly specified in regular CSS.

Then I came across Tailwind in one of the projects at work. My initial impression was, 'What the hell is that monstrosity of classes in the markup?' My IDE was not wrapping it well which resulted in a single line of markup to span across multiple lines causing readability issues. I had to always keep multiple tabs of Tailwind docs open, and go back and forth between the docs and code. More than everything else, my brain was hardwired to use BEM class names, and having to write utility classes was too much effort. Constantly, I had to go back and forth between the docs and styles.

Fast-forward a couple of months, now I actually use Tailwind over regular CSS for any quick run-of-the-mill application, or that 3am experiment I was doing. I've been using Tailwind for a few months now, and I'm happy with the result.

For this iteration of my site, I decided to stick with Tailwind (which was part of the starter template, so no additional effort again!)

Oh, and dark mode. Tailwind has a wonderful API to support dark mode, and it was extremely easy to set up. For the first time in 6 years, my site now supports light and dark mode. Click the lightbulb icon to switch themes

Tailwind still has its drawback though. For example, the current page title markup looks like this:

export default function PageTitle({ children }) {
  return (
    <h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
      {children}
    </h1>
  )
}

For simpler components it's great, but as soon as one starts adding variations for dark mode, typography and media queries, it just starts growing. I'm dependent on the built-in integration of Tailwind with WebStorm, so it wraps the classes neatly and provides great auto-complete, which so far, has kept me productive. It's a win-win! (At least for my personal projects and smaller apps)

Tailwind CSS is somewhat controversial, it seems to have an equal number of fans to haters. Using Tailwind for some time on projects, I empathize with both sides. My team also made a conscious decision at work to migrate away from and deprecate Tailwind when we were updating a legacy project which used Tailwind. It was driving down team productivity and new developers often faced a steep learning curve when encountering Tailwind for the first time. And with the ever-changing design requirements, Tailwind was getting in the way more than it helped us.

As with anything, Tailwind CSS has both advantages and trade-offs. Pick what works for you and your team, and see if the trade-offs are worth it. At the end of the day, it's just another tool made with good intent.

MDX

MDX is Markdown for the component era. It allowed me to write React code inside Markdown files, and render it. I could use a custom React component for a h1 tag, or build an entirely new component for a markdown construct. Since the starter template used mdx-bundler, I just rolled with it.

I did, however, use my own implementation of rehype and remark plugins to parse, and add additional props to the React component like Admonitions and Code blocks. (see the markdown kitchen sink here for all the supported constructs).

All my posts were in Markdown format. There wasn't any need to update old posts apart from some typos and language sentiments. Which meant I could just copy over the existing Markdown files and call it day. Well...not quite.

Remember how Gatsby uses GraphQL for everything? Well in Next.js, it's manual work.

getStaticProps and getStaticPaths

Next.js will statically pre-render all the paths specified by getStaticPaths. The paths for each article are defined in the front-matter of the post. Next.js will pre-render pages at build time using the props returned by getStaticProps.

The issue here was that, getStaticPaths and getStaticProps does not share data. Which meant reading and parsing the Markdown files twice. Once for generating the path from front-matter, and second time to read the Markdown content and pass it as props to pre-render the page.

---
title: 'Goodbye Gatsby, Hello Next'
author: Dhanraj Padmashali
tags:
  - Next.js
  - JavaScript
  - Gatsby
  - Tailwind
image: /static/images/moving-day.jpg
draft: true
permalink: upgrading-blog-to-nextjs
excerpt: ''
---

As the content grows, the build process becomes noticeably slower. And for pages where only the meta-data is shown, it's not logical to read the entire Markdown file.

To overcome this, I wrote a script that generates an indexing file. The indexing file contains the front-matter for all the articles. So now instead of reading the Markdown file and parsing it twice, it just reads the indexing file for paths, and parses the Markdown file once. The build process is much faster this way, and saves precious minutes on deploy. (Minutes is the new currency)

Images and Next.js

Gatsby had a great Image component. gatsby-plugin-sharp took care of image sources, injecting it to the GraphQL layer. All I had to do was:

<GatsbyImage
  id="fnref-cover-photo"
  className={imageWrapper}
  image={image?.childImageSharp?.gatsbyImageData}
  alt={title}
/>

This basically optimized images at build time, and the images loading experience was quite nice. However, build times depended on the number of images.

Enter next/image. Unlike Gatsby, which optimizes these images during build time, Next.js uses an image optimizer (URLs that look like /_next/image?url=...&w=640&q=480) that returns an optimized image on-demand as the image is visible on the viewport.

For now, I'm going to stick with the default image optimization strategy that Vercel employs. I might look into services like Cloudinary, or even use a CDN if the performance worsens over time due to large images.

Comments and Feedback

The last iteration of my site used an open-source tool called Utterances. This time I switched over to Giscus. It uses GitHub Discussions instead of Issues, which seems like a great idea. Since Giscus requires to log in with GitHub and grant permission to post on their behalf, there are social sharing links at the end of each article, for people who aren't keen to sign up for Giscus.

The deployment conundrum

Ever since I've owned this domain, I've pretty much used Firebase Hosting. It has a generous free tier which I've never exhausted.

I now wanted to give Vercel a shot. One major advantage of using Vercel is their focus on Next.js. The CI pipeline, features, and optimizations revolve around front-end tooling. Vercel also allows deploying multiple versions of the same site, dubbed preview sites, allowing you to deploy feature branches under different subdomains. There are also some auto-magic applied by Vercel, for image caching and static asset optimization. I'm not sure if I'll ever get to see the full power of Vercel, as I'm going to be sticking with the starter plan, but I'm excited to see what it can do for me.

Where is the Source?

Alright, if you're with me so far, you may be wondering if you can take a peek at the source. Unfortunately, I do not intend to make this blog/site open-source...yet.

I don't have a great track record to write and publish content periodically. Sometimes I publish a bunch of content every day for a month. Other times, the content just sits as draft for months together. If I were to make this blog/site open-source, all of those drafts would be public. Not really a fan of that.

I work on this site in my spare time. Being a personal site, I tend to leave in a lot of dead code. (like years worth of code, which I don't want to delete because I may use it someday) There are also code paths which I am not proud of, but if it works, it ships. 🤷🏽‍♂️ I also don't tend to write test cases immediately, because this is not a mission critical site. If I were to make this site open-source, I'd have a nagging voice in the back of head to make sure everything is well documented, with working tests so that people have very less friction when they use my code. I hate that nagging voice.

Therefore, this site remains closed-source, for now. If there's some aspect of the site that you want to borrow, feel free to reach out, and I'll help in any way I can.

Where to from here?

This would be the 6th iteration of my site. I'm actually impressed at design I came up with, and the custom Markdown constructs using remark plugins. It was a fun experience designing components like Code blocks and Admonitions. Got my hands dirty with rehype and remark plugins. Used a ton of Tailwind. Overall, a great learning experience.

Lighthouse Score

Fun Fact: I lost almost 3 months of work I did on this site, because my MacBook decided to die one fine morning. I had everything under source control, but I never got around to pushing the code to a remote repository.

I learned that the hard way, and now I have an automated job that pushes to remote repository periodically.

Thanks for reading. See you around!