Node 18 Ate My Website

And the technical decisions I made building a new one.

July 1, 2024

A quick summary

Due to the deprecation of Node.js 16, I upgraded my Next.js website's runtime from Node.js 16 to Node.js 18. Unfortunately, this broke things quite a bit. After extensive dependency wrangling, and with no end in sight, I decided to build my site anew with a fresh coat of paint and a more maintainable architecture. While doing so, I made a lot of technical decisions and dependency changes. My reasoning for the most notable can be found below.

A note on formatting: I use backticks, ``, below for anything that shows up literally in code (e.g. dependency names, file names). When talking more generally about technologies, I use their colloquial naming and no backticks.

So, what exactly happened?

A few months ago, I went to add a blog post to my website and hit a snag. I wrote the post, added it to my repo (as a .mdx file read by my Next.js app), triggered a Vercel deployment, and walked away.

I came back to the following error:

Error: Node.js version 16.x has reached End-of-Life. Deployments created on or after 2024-02-06 will fail to build. Please set Node.js Version to 18.x in your Project Settings to use Node.js 18.

"Not a big deal," I thought.

This error wasn't blocking my deployment at the time, but it would do so down the road, so I decided to go ahead and make the upgrade from Node.js 16 to Node.js 18 while I had time to troubleshoot.

I updated my Vercel runtime to Node.js 18, redeployed, and stepped away once again.


When I returned, I faced another error; a familiar foe.

Error: error:0308010C:digital envelope routines::unsupported

The upgrade was not going to be as simple as I had hoped.

The above error occurs when a dependency attempts to use an OpenSSL version that is no longer supported, and it comes up often when upgrading from Node.js 16 to Node.js 18.

The solution typically involves a good amount of dependency wrangling, and a little more art than science.

In this case, the dependency throwing the error was the babel-loader package that shipped with my next version.

To resolve it, I needed to determine:

  1. what minimum version of babel-loader was compatible with Node.js 18,
  2. what minimum version of next I would have to upgrade to to support that babel-loader version, and
  3. what versions of react and react-dom I needed to use to support that version of next.

Figuring those out, which took a good bit of digging, I made the changes to my package.json, crossed my fingers, and redeployed.


TypeError: Cannot convert undefined or null to object

Progress, but another dependency issue to resolve.

The problem this time was with the prebuild-webpack-plugin package used by the next-mdx-enhanced package I was using for MDX handling.

To get MDX handling working again, I would had to figure out:

  1. what minimum version of the prebuild-webpack-plugin package I needed to resolve the error, and
  2. what minimum version of next-mdx-enhanced I would have to upgrade to to support that version.

But the trail ran cold.

Version 5.0.0 of next-mdx-enhanced did not use a version of prebuild-webpack-plugin that would resolve the problem. And next-mdx-enhanced was deprecated after 5.0.0.

That meant, if I was going to continue along this upgrade path, I would need to rewrite the way I handled Markdown/MDX files in my app.

At peace with that, I commented out all Markdown/MDX functionality and pressed on.


Another problem. Then another. And another.

I resolved each as they came up, but there was no end in sight...

And progress was slow...

And I eventually decided to take a step back and consider my options.

My decision to build anew

At this point, there was no telling how long it would take to get a working version of my site up and running. And it wouldn't even be the same site. Yes, the content might be the same, but what unexpected side effects would all of these dependency changes have?

It felt like I was doing a messy renovation when I could build something better from scratch in about the same amount of time.

So I did just that. I built a much more refined and maintainable website than I had before, had a lot of fun going through the process, and learned a lot along the way.

I've recorded the notable decisions I made and my reasoning below in hopes that it is helpful to you or someone you know.

First I'll go over the key components of my original stack. From there, I will talk through what I wanted to keep, what changes I ultimately made, and a few other technologies I considered.

My original stack

My site has been built around many technologies over the years. From Hugo to Jekyll to WordPress to Ghost with just about every popular CSS library under the sun, it has been a place for me to build and test my skills.

Before this upgrade attempt, my site was built around Next.js on Vercel using the original pages router. All content was generated statically. Blog posts were composed as .mdx files and handled by next-mdx-enhanced. Chakra UI was used for styling. Umami was used for analytics. Remark plugins were used for adding things like automatic slugs and heading links to my posts.

More granularly, my original stack looked like this:

  • Next.js with pages router — for building the site as a React app with lots of additional optimizations and niceties out-of-the-box.
  • Vercel — for hosting and many DX benefits like automated preview deployments.
  • next-mdx-enhanced, @next/mdx, @mdx-js/loader, and @mdx-js/react — for MDX handling with a few enhancements like layouts and frontmatter consumption.
  • Chakra UI — for styling with pre-built, accessible components.
  • Emotion — a peer dependency for Chakra UI. Not used otherwise.
  • Umami running on Railway — for privacy-friendly, basic web analytics.
  • Next SEO — for simple SEO handling (mainly metadata generation) with Next.js.
  • mdx-prism — for code syntax highlighting.
  • Several remark plugins — for making helpful adjustments to markdown output (e.g. automatically adding slugs).
  • ESLint — for linting.
  • Prettier — for formatting.
  • Husky and lint-staged — for enforcing linting and formatting on each commit.
  • Lots of other small dependencies not worth mentioning.

What I liked about my stack and wanted to keep

Let's talk about the things I liked about my stack and wanted to keep:

  • Ease of composition brought by Markdown and MDX support. Markdown is a joy to write with. It takes away the distraction of formatting and allows me to focus on content.
  • Static generation and strong SEO provided by Next.js. There are a lot of strong opinions about Next.js out there, but it has worked well for me. It is performant, handles SEO well, and I already knew it well. Sticking with it made sense to save time.
  • Ease of deployment brought by Vercel. Similar to Next.js above, a lot of folks have strong opinions about Vercel. But I've had a good experience with it and didn't see a good reason to migrate. I don't get a ton of traffic and my site doesn't have a lot of moving parts, so I don't have to worry about their pricing model and I get to enjoy the many benefits of their platform.
  • Really solid performance. Thanks to Next.js and an intentional design, my website is fast. I love and prioritize that. I like how premium and snappy it feels.
  • Accessibility. Chakra UI provides accessible components by default. Whether I kept Chakra around or not, I wanted to retain the accessibility it provided.
  • Decent printing out of the box. My site is designed to print well. If a reader wants to print out a post and read it on the go, it's going to look good and read well.

The changes I ultimately made and why

Making sure to keep the above, I also made a lot of changes. Most notably, I:

  • Moved to TypeScript. I now write all code in the JavaScript ecosystem with TypeScript. Autocomplete is just too helpful not to, so I adjusted my website to match.
  • Upgraded Next.js and moved to the new app router. I upgraded Next.js to the latest version to give myself some runway. I also switched to the new app router because it resembled the new Expo router, which I liked, and it seemed like the direction in which Vercel was pushing things. I didn't want to have to restructure again in the near future.
  • Replaced existing Markdown/MDX handling with Contentlayer. Used by @delba_oliveira and @leeerob at the time, Contentlayer seemed like a strong, type-safe option and the way the industry was headed. It also allowed me to source Markdown from other places (like pulling directly from Obsidian in another repo) if I wanted to. Plus it meant two dependencies instead of four (excluding remark/rehype plugins) for Markdown/MDX handling!
  • Replaced Chakra UI with Tailwind CSS. I decided I didn't want a component library, and I had gained a lot of Tailwind CSS experience since I built the previous version of the site. Switching to Tailwind was quick with no downsides.
  • Moved from remark plugins to rehype plugins. Some of this was forced because the packages themselves changed, but making changes to HTML after conversion makes more sense to me than making changes to the Markdown before conversion. More flexibility.
  • Moved from mdx-prism to rehype-pretty-code for code highlighting. rehype-pretty-code uses shiki under the hood and is really solid. It seemed like the direction in which markdown code highlighting was going.
  • Moved from Umami to Vercel Analytics. With a small number of visitors on my site, I couldn't justify the overhead of hosting Umami on Railway. It just didn't make sense when Vercel Analytics is such a strong offering (at least until you have to pay for it).
  • Transitioned from Yarn to PNPM. PNPM is more intuitive and performant than Yarn at this point. And it just feels better.
  • Added prettier plugins for import and Tailwind CSS sorting. To improve the developer experience a bit and remove mental overhead.
  • Removed lint-staged and Husky. These are both great, and I generally like to use them, but I'm the only one working on my site. And I already autoformat, so additional dependencies seemed redundant. I've generally moved away from tooling like this and things like Conventional Commits, Commitizen, and Commitlint because the overhead seems to outweigh the value.
  • Added automatic Open Graph image generation. To make posts shared on social media look better by default.
  • Removed lots of small dependencies. I decided that the fewer dependencies I had going forward, the easier the site would be to maintain. And that has been the case so far.

Some technologies I considered but decided not to use

While researching and making the changes above, I also came across and decided not to use the following notable technologies:

  • shadcn/ui — This was gaining a ton of popularity at the time. I just don't like the idea of opting into component libraries without a good reason. You lose so much flexibility and have to learn a new system. The better I get as an engineer, the more I like sticking to the primitives. I did reference some of the styling, though.
  • Radix UI, Headless UI, and React Aria — A level down from shadcn/ui, unstyled components still feel too much like component libraries to me. I'd rather stick to my own abstractions and keep things simple. Of the three, though, I liked Headless UI the most because I trust the Tailwind CSS team to maintain it. Radix UI is being used by shadcn/ui, which is sort of being supported by v0 and Vercel, so it will probably be around for a while, too.
  • Nextra — A framework for building MDX websites on Next.js. Too opinionated for my taste, and there was no good blog starter. I couldn't justify adding another layer on top of Next.js for the value it provided.
  • Volta — Great solution, but I've settled on using corepack out of the box instead. I still have to manage node versions myself, but nvm has worked well for that for a long time.

Conclusion

What started as a simple upgrade ended up as a full rebuild, but my site is much better for it and I had a lot of fun. I wish I had come across a post like this while I was researching, so I decided to write one myself.

If you are interested in more posts like this or want to work together, please reach out! Goodbye for now!

Thanks to @delba_oliveira, @leeerob, @rauchg, @shuding_, and @shadcn for inspiring some of the decisions above.