Dark prism

Migrating to Next.js 13

Published on
Last edited on

Written by Robert Koch

9 min read
Over the last couple of weeks I've been updating this blog to use the new layouts system in Next.js 13. With the publication of this post I am very excited to announce that this site is running completely on next.js 13 and server components via Vercel edge functions.
It's been interesting to refactor and while the official upgrade guide covers a lot of the basics, there are still some gotchas that can trip you up. I'm going to document all the issues I had migrating to Next.js 13 and how I solved them.

Installing the latest version of Next.js

To start with I followed the same steps in the upgrade guide, I installed the latest version of next and react via npm.

npm i next@latest react@latest react-dom@latest

This went fine and without a hitch.

Running Codemods for the latest functionality

After this, the next step is to upgrade to the latest features in the built-in components like Link and Image. Next has a tool called codemod that will scan your entire project looking for updates that need to be applied to use the new features.

You do need to commit any changes before running a codemod command otherwise it will not work.

The first step is to convert all next/image imports into next/legacy/image imports as version 13 of next.js has a new image component that breaks some functionality with the old component.
npx @next/codemod next-image-to-legacy-image .
Additionally, if you want the new Image functionality you can run the following codemod to update all Image components to the new one.
npx @next/codemod next-image-experimental .
Now this should not break any images you have on your page, but there's no guarantee depending on how you set up the page.
Finally you can update any Link components to use the new functionality where they don't need an a tag inside them.
npx @next/codemod new-link .

Now I found that most of these codemods worked fine, but there were some edge cases where the updates didn't do what I expected so make sure you manually check to see what the difference these codemods perform.


Of course the big new feature of the new version of next is the app directory which uses a completely new layout system. Now it's possible to build a page using a tree structure to render components asynchronously from each other. Say for example you have a common layout that all your pages use (like myself), you can now specify this layout in the root of the app directory in layout.tsx.
1<div className="min-h-screen flex flex-col overflow-hidden">
2 <Topbar />
3 <div className="flex-grow">{children}</div>
4 <Footer
5 title={'Kochie Engineering'}
6 links={
7 // ...
8 }
9 />
Now this structure will apply to all the pages nested within the directory. In terms of migrating to this structure the best way to do it is to move one page over at a time. Beware however as it's not as easy as copying the content over, for example, the Head component no longer works and you now require a head.tsx for every page.tsx file you have if you want to manipulate the head section of the page.
├── _app.tsx
├── _document.tsx
├── _error.js
├── articles
│   └── [articleId].tsx
├── authors
│   ├── [authorId].tsx
│   └── index.tsx
├── index.tsx
└── tags
├── [tagId].tsx
└── index.tsx
This changed my file layout from the pages directory (seen above) to this new app directory below.
├── articles
│   └── [articleId]
│   ├── head.tsx
│   └── page.tsx
├── authors
│   ├── [authorId]
│   │   ├── head.tsx
│   │   └── page.tsx
│   ├── error.tsx
│   ├── head.tsx
│   └── page.tsx
├── head.tsx
├── layout.tsx
├── page.tsx
└── tags
├── [tagId]
│   ├── head.tsx
│   └── page.tsx
├── head.tsx
└── page.tsx
At the time of writing you can't change the title when navigating from another page without editing it in the page.tsx file. This is getting fixed.

One issue I had early on was many pages crashing the server due to incompatible server-side components. The error was similar to the one below.

Warning: Only plain objects can be passed to Client Components from Server Components. global objects are not supported.
Warning: Only plain objects can be passed to Client Components from Server Components. global objects are not
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'global'
--- property 'global' closes the circle
To fix this unfortunately you need to go through each component and use a process of elimination to figure out what component is causing the error. Once you find the right component you can just use the use client directive at the beginning of the page.

Font Awesome

I had some issues with Font Awesome's react components not working properly with server-side rendering. These issues seem to be fixed now but look out for issues related to loading the library still.


Tailwind is fortunately pretty easy to update, all you need to do is change the content property in your tailwind.config.js file to the following.
module.exports = {
content: ['./src/app/**/*.{js,ts,jsx,tsx}'],
This makes sure that tailwind scans the new app directory for any css classes.

Fathom Analytics

I'm using Fathom Analytics to track pageviews on this site and I've had to make a few changes for it to work. Originally the way to use it was well documented but now with the new layouts system the useRouter hook that this solution depends on doesn't work anymore. The solution I've found is to create a client component in the root layout that updates when a new layout path has changed.
1'use client'
2import { load, trackPageview } from 'fathom-client'
3import { useEffect } from 'react'
4import {
5 usePathname,
6 useSearchParams
7} from 'next/navigation'
9export default function Fathom() {
10 const pathname = usePathname()
11 const searchParams = useSearchParams()
13 useEffect(() => {
14 load('XXXXXXXX', {
15 includedDomains: [''],
16 spa: 'auto',
17 })
19 trackPageview()
20 }, [pathname, searchParams])
22 return null

This client component is loaded on the initial page load and the hook will fire when the path changes or parameters change.

OpenGraph and SEO

OpenGraph is a bit interesting, I'm using the next-seo package to generate most of the SEO tags for my page and a custom puppeteer script to create the OpenGraph images. Recently and unrelated to Next13 chromium has not been installing properly in Vercel build scripts so I've had to migrate over to Vercel's library to create the images. The new @vercel/og library uses edge functions to generate the images on the fly, in the beginning I had some issues getting it to work properly with the styling I wanted but I've been able to configure it to my liking.
As for SEO meta tags, the updated next-seo package provides support for the app directory supports a similar pattern for defining the needed tags. For each page.tsx you can export a head.tsx and have the head render for each page.
1export default async function Head({ params }: { params: { authorId: string }}) {
2 return (
3 <>
4 <NextSeo {...NEXT_SEO_DEFAULT} useAppDir={true} />
5 </>
6 )

MDX Rendering

Initially I was stuck on how I would implement mdx rendering with the new system. Previously I was using next-mdx-renderer which seemed to break on the latest version. But there was a trick I saw shadcn use in their latest app taxonomy that helped me fix the issues I was facing. See with the new split of server and client react components you have to decide what parts of the interface should be built in the clients browser.
By creating a wrapper around the MDXRemote component I was able to make the mdx render on the client side and not the server. This is a pattern I've used throughout the project and it's quite useful.
1'use client'
3import { MDXRemote, type MDXRemoteProps } from 'next-mdx-remote'
4import * as components from './components'
5import React from 'react'
7export const MDXContent = (props: MDXRemoteProps) => {
8 return <MDXRemote {...props} components={components} />

I don't exactly like this solution, mainly because it would be more efficient for the mdx to be compiled and rendered on the server side and only required client components sent down the wire, but for now it seems to work.

Progressive Web Apps (PWAs)

Next.js does not support PWAs out of the box, but there is a package called next-pwa that provides the functionality. At the time of writing next-pwa does not support the new app directory, but there has been a pull request for a few weeks now that does add the functionality. Hopefully once it's merged in there will be feature parity between the pages and app directories.

Should you refactor to use server components?

For the amount of work required and the number of bugs and compatibility issues, I would say no. The reality is that the new layouts system is still in beta and even Vercel does not suggest running it in production.

If you're working on a new site I think it's worth using instead of the old pages setup but refactoring old sites is not needed, I haven't seen a significant speed increase in my static site.

The real advantage of using layouts is the ability to interact with the react suspense pattern and render different parts of a page while other parts are still loading, allowing for a more dynamic and interactive experience.

In closing, the new layouts system will soon become the defacto way to build websites with Next.js, but the effort required at the moment to refactor a webpage is still too high to justify the benefits.

Robert Koch Avatar

👋 I'm Robert, a Software Engineer from Melbourne, Australia. I write about a bunch of different topics including technology, science, business, and maths.

Like what you see?

Find out when I sporadically scream into the void...

Privacy respected. Unsubscribe at anytime.