When I built this site in 2021, I had been aware of Next.js but never used it. I was excited to have a project that would allow me to learn the ins and outs of Next. At that time, I used version 12, and since then, I’ve been following the developments of Next on the Vercel YouTube channel, Hacker News, and r/nextjs. I was excited at the release of version 13 and all the enhancements that came with it. I knew I’d eventually find time to upgrade, but I didn’t prioritize it. I had some time off from work towards the end of the year and found a couple of days to go heads down in the updated docs and update this site. In addition, I also moved all my content from the
/pages directory into the
/app directory (which is still in beta).
I learned a few things while upgrading that weren’t clear in the official upgrade guide. I also took the opportunity to do a few cleanup things that I’ve wanted to do for a while. And, yes, it would have been cleaner/safer to keep those items separate from the upgrade, but this is my site and project, and I do what I want.
You can view the entire changeset on GitHub if you don’t want to read words and only look at the code.
I followed the official upgrade guide step by step. As this was my first time digging into the beta docs, I did a lot of bouncing around, reading, and re-reading sections and pages of the docs. For the most part, the upgrade guide is an excellent resource. I got through most sections without issue. There were areas that held me up and I will explain those problems and what I did to solve them. If you’re upgrading and have questions, please drop them in the Comments section below.
The Next team offers two codemods to help you migrate to the new image component. However, I didn’t find either of them useful.
next-image-to-legacy-image updates the import to use the legacy
<Image /> component, which I didn’t want to do.
next-image-experimental does its best to modify existing
<Image /> components to match the new conventions. Since I have a component that wraps
next/image this codemod wouldn’t work for me.
So I found all the places where I imported my
<Image /> component and manually updated them. Luckily for me, there were only four places. Updating the props used wasn’t intuitive, but between the beta docs and current docs for Image optimization and the
next/image docs, I got the information I needed to update the components accordingly.
The upgrade guide helped me get most of the way to working except for 1) Font name conventions and 2) use with Bootstrap.
Font name conventions
The docs use the font Inter as an example. When importing the font from the
@next/font/google package, you destructure the font from the package using its name. So in the case of “Inter”.
However, they do not mention the convention when importing fonts with more than one word in their name, such as “Roboto Mono”. After trying a few things and searching around, I found this Next.js 13 + Google Fonts post on Medium, which confirmed what I had seen by testing: Fonts with two words use an underscore in place of the space.
I was able to get the answer I needed by trial and error and searching, but it would have been nice to see that as an example or mentioned in Next’s docs.
Yes, I still use Bootstrap. I wrote a bit about my decision to use it over Tailwind on my Built With page. Because of how Bootstrap works, I couldn’t just add the font class name to the
<html> tag. Next does provide an example for using
next/font with Tailwind, which helped point me in the right direction.
To work with Bootstrap, first, include the
variable property to define a CSS variable for the font. The variable name (
--font-roboto) can be whatever you’d like.
Include the font’s
variable property as a class name to the
bootstrap.scss file to use the CSS variable when setting the font family.
You can view my full implementation in the
app/layout.js file here.
Migrating from pages to app
I went back and forth about opting into the app directory, as the Next team is very clear that it is still in beta. They even go as far as to recommend not using it in production. I’d heed that warning if this were a client site or something for work. But, again, this is my site, and I do what I want.
Overall, migrating the five or so pages was pretty straightforward, and following the guide proved to be very useful. There were a few hiccups, though.
All the pages on my site are based on static files available during build time. For example, the
/posts/[slug] URL points to the
app/posts/[slug]/page.js file, and only the static files available in the
_posts directory should count for valid URLs. Everything else should 404.
Here’s the output from a recent build.
Route (app) ┌ ○ / ├ ● /[page] ├ └ /about ├ ○ /built-with ├ ○ /posts ├ ● /posts/[slug] ├ ├ /posts/3d-printer-psu-control ├ ├ /posts/nextjs-version-13-beta-upgrade ├ ├ /posts/rack-mounting-home-assistant-yellow ├ ├ /posts/unifi-g4-doorbell-chime-with-sonos ├ ├ /posts/using-custom-svgs-with-fontawesome ├ ├ /posts/wireless-ecobee-mini-split-home-assistant ├ └ /posts/wled-christmas-lights └ ○ /youtube ... ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
This means I don’t need true dynamic URLs where the lookup if a page exists happens server side. In my case, I was getting a runtime error where the URL
/favicon.ico was throwing a server error because it was attempting to be routed through the
There are two solutions to this problem.
For static URLs generated with the
generateStaticParams function, we can export a
dynamicParams const as
false. According to the docs for
dynamicParams, “Dynamic segments not included in generateStaticParams will return a 404” when the value is
The other way is to use the
notFound() function provided by the
You can find more information on
notFound() in the docs.
Server Components + Client Components
With the concept of Server Components and Client Components, Next.js 13 defaults files within the
app director to server components. You can force any component to be a client component by using the
"use client" directive at the top of the component file.
use client directive in four places across the site.
Three of the four components that define the
use client directive truly need it.
NavToggle all interact directly with the DOM in some way and cannot be rendered entirely on the server.
The one exception is the component I created that renders MDX (Markdown) content which uses the
next-mdx-remote npm package. The Next team is still working on proper support for MDX in the
app directory, so I’m happy to use this stopgap until they’ve sorted it out.
As a development tool, there is also the
server-only package. You can ensure that a client component doesn’t import the file by importing the package into a file. While in development mode, the package will throw a warning in the console, letting you know of your misdeeds.
The last pain point I dealt with during this upgrade was the new
head.js is a new “special file” Next introduced that allows different route segments to configure the
<head> tag of the corresponding page.
I use a combination of the
next-seo package and my own
<Meta /> component to configure and output all the SEO related meta tags I’d like on any given page. Luckily for me, the
next-seo maintainers are already working on and have documentation for supporting the
app directory. Their work made my life easier in understanding the limitations of
head.js and getting things working the way I expected them to.
In making the upgrade, I had to create the new
head.js file for all my route segments. Doing so meant I had to fetch a page’s data twice, once in
head.js and again in the main content
page.js. The Next team says, “When rendering a route, Next.js will automatically dedupe requests for the same data across layout.js, page.js, and head.js.”. I have yet to see how they’re doing this and if it’s working, but it feels clunky to make the same request twice for a single page.
Lastly, I had to restructure how I set meta tags for each page. From
'next-seo’s doc, “Next.js no longer de-duplicates tags in the head.” Removing the deduping of tags means instead of updating similar meta tags across different pages; new meta tags will be appended to the existing ones.
Now, the Next team has stated, in a few places, that they are still very much working on how
head.js works and the APIs they provide to support frequent use cases. I look forward to seeing what they come up with.
As I mentioned earlier, I took the opportunity to complete the following cleanup tasks.
defaultProps with default function parameters
In React 18.3
defaultProps will be deprecated, and I was getting console warnings about it, so I took the opportunity to convert the handful of components that used
defaultProps to use default parameters.
The migration was relatively easy and there weren’t any gotchas.
/app directory uses directories (and nested directories) to define routes, I found my import paths were becoming quite ugly.
For example, the posts
page file on my site lives in
app/posts/[slug]/page.js, which makes any import from the root directory look like this.
And for the posts listing page at
app/posts/page the import path is
Keeping track of how many levels deep a given file is and updating the import path is cumbersome and slows me down.
baseUrl option allows me to ditch the many levels of
../ in my imports and always work from the root directory. So no matter where a file is in the directory tree, I can import a file like so.
Hopefully, others thinking about, or in the process, of upgrading to Next.js 13 find this writeup helpful. Overall my experience with Next continues to be an enjoyable one. I’m excited to see the progress and momentum it has gained recently.
Found the above useful? Have a question? Thought of something I didn't?
Consider leaving a comment below or send me an email at firstname.lastname@example.org.
You can also buy me a coffee. Cheers!