piratematt.com v3—Upgrading to Next.js 13
Tags: nextjs, personalwebsite, nextjs13, upgrade
Published: 2023-07-19 15:30:53

_Series: **<series>Personal/Professional Website</series>**_
_Sub-series: **<sub-series>piratematt.com v3</sub-series>**_
_Entry: **<entry>02—Upgrading to Next.js 13</entry>**_
_First Published: **<first-published>July 2023</first-published>**_ 


## Plans never survive contact with reality

This wasn't supposed to be my second entry, but much like the [Agile Manifesto](https://agilemanifesto.org) I value responding to change over following a plan. Next.js released their next major version, Next.js 13. Things are different… _How different_ is what this entry chronicles. Well… chronicles for a simple static site that is exported and served via github pages. If you're looking for a full comparison I suggest you start with [their own blog post on version 13](https://nextjs.org/blog/next-13). It's where I started and I found it helpful.

## The origin of the pivot

This upgrade journey began when I reached for Next.js to start a new project with a friend and was surprised by the [app router](https://nextjs.org/docs/app/building-your-application/routing#the-app-router). Caught flat footed I scrambled to explain a framework I no longer understood. Rather than simply reverting to an older version I was familiar with, we took some time to read through the docs and get the new lay of the land. Long story short, the project itself is on hold while my friend gets settled into a new job, so I switched my effort to understand Next.js 13 to my personal site. It certainly doesn't _need_ updated, but I want the practice, so here we are.

## The work ahead of me

Borrowing a technique from TDD (I'm still new-ish to TDD, but I've been making a concerted effort to practice it lately), I got started by creating a lightweight need/want TODO list. This enables me to give ideas the space they deserve as they crash into my brain, but in a way that lets me keep focus on the small step I'm trying to take when the crash occurs. My initial list was built from reading through the aforementioned blog post as well as the [upgrade guide](https://nextjs.org/docs/pages/building-your-application/upgrading). These initial reads are done somewhere between a skimming pace and a studying pace. The goal isn't to recall everything, but to start to get a feel for the lay of the land. _What should I look out for? Where can I read more about how to get XYZ working? Should I switch to the shiny new ABC way of doing things, or stick with what's already got working?_

I'm not going to include my actual TODO list because it's not a good communication tool. It's a good working tool. It's constantly modified; items get added, removed, re-added, and re-removed all the time. You get the picture. To outline the work ahead of me I'll split things into two lists.

List 1: The differences in Next.js 13 I want to tackle in this effort.

* Switch to the new `next/font`.
* Migrate from the _pages router_ to the _app router_.
* Minimize my use of the `use client` directive (prioritize server side components).

List 2: A simplified overview of how this reflected in my TODO list.

* [ ] upgrade to latest Next.js version
* [ ] upgrade ESLint version
* [ ] upgrade to Built-in Font
* [ ] switch from pages to app router
  * [ ] identify server vs client components and update appropriately
  * [ ] rework how layouts work
  * [ ] refactor `<Head>` components to `metadata` exports
  * [ ] refactor from styled-components to CSS modules (styled-components require runtime javascript and therefore must be client components)
  * [ ] refactor from pages/404.js to not-found.js file

## Task 0 – Create a home for the effort and update to the latest versions

I wanted a safe isolated place in my github repository where I could tackle this update effort, so I kicked things off by creating a new branch: 

```zsh
git checkout -b next13
git push origin next13
```

Now I have a safe space I can frequently push my WIP (work in progress) states to just in case my local machine melts.

So… it turns out I'm already using Next.js 13, `"next": "^13.1.6",` to be exact. _What a twist! <sup>[ref](https://www.adultswim.com/videos/robot-chicken/what-a-twist)</sup>_. How did I uncover this? As I was doing my first readthrough I saw they called out that `<Link>` components no longer needed a child `<a>` tag to function, and I thought to myself I've _never_ had to do that to get `<Link>` to work. So I took a quick peek at my package.json git history… and, yup, I've always been on Next.js 13. Fortunately for my sanity it's clear looking at the [nextjs.org docs](https://nextjs.org/docs), that there has indeed been a recent switch to the _app router_, so my confusion wasn't merely the result of some fever dream. 🤞🏻being on Next 13 already makes my “upgrade” easier.

To get the latest and greatest for both Next.js and ESLint, I run:

```zsh
npm i next@latest react@latest react-dom@latest eslint-config-next@latest
```

This puts me at the following version (extracted from package.json) 👇🏻

```json
{
  "dependencies": {
    "next": "^13.4.7",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
  },
  "devDependencies": {
    "eslint-config-next": "^13.4.7",
  }
}
```

I then execute `npm run lint` in my terminal and fix any reported issues (that aren't a future TODO list item).

## Task 1 – Switch to the new `next/font` and away from the `@next/font`

After a brief read through the [Font Optimization section of the upgrade guide](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts), I try the following:

```typescript
// import { Share_Tech_Mono } from '@next/font/google';
import { Share_Tech_Mono } from 'next/font/google';
```

After a bit of manual checking, I realize this is all I need to do, so I also go ahead and uninstall the old `@next/font` and delete the commented out import line.

```zsh
npm uninstall @next/font
```

That was quick and painless! Another item checked off my TODO list! 🎉

## Task 2 – Bye-bye “Pages” router, hello “App” router

I wish I could tell you this was an easy process with clear steps that you can follow in the same order I accomplished them in and succeed, but it ended up being more of an ever expanding FILO (first in, last out) stack of TODOs. I'll do my best to use headers to provide some clarity, but things may jump around a bit.

### Task 2.1 – Refactor the home page to use the _app router_

Trying to take the smallest step I could, I set my targets on replacing the existing home page, `pages/index.tsx`, with a “hello world” home page powered by the _app router_. To get started, I added an `/app` directory and placed a hello world `page.tsx` file within it.

```tsx
// app/page.tsx
export const metadata = {
  title: 'Home – piratematt.com'
};

export default function HomePage() => {
  return (
    <h1>Hello Home Page</h1>
  );
}
```

Then I changed the name of my `pages/index.tsx` to `pages/old_index.tsx` to prevent conflicts, and restarted my dev server:

```zsh
npm run dev
```

A quick navigation to `http://localhost:3000/` showed me my shiny new “hello world” home page. Great! Things are moving along. Next I simply copy the contents of my old home page, styled-components and all. Optimistically I refresh my page. Uh oh, things are no longer working. None of the updates I made to `_app.tsx` and `_document.tsx` to get styled-components working are taking effect. Hmmm… time to add a new task to the stack.

### Task 2.2 – Refactor `_app.tsx` and `_document.tsx` to the new `app/layout.tsx` structure

Skipping a lot of narratively unnecessary trial and error let's just say this is where my desire to preserve my use of styled-components left me. I discovered each page/component leveraging styled-components needed the `use client;` directive, which felt wrong because most of my page content is static “brochure content” and has no business not being rendered server-side. To that end, I elected to add another task to my quickly growing stack: _refactor to CSS modules and prioritize server-side rendering_. (Yes I realize that exporting this specific Next.js project makes it of little consequence in the end. Nevertheless, I saw value in practicing limiting my client-side components.)

### Task 2.3 – Refactor styled-components (CSS-in-JS) to CSS modules

This is a task that gets repeatedly put back on the top of my task stack as I tackle pages one at a time. Rather than bore you with that repetitive process, I'll leave a sample of some salient details and move on. Overall the process was fairly simple. Rather than having styles declared inline within my `.tsx` files, declarations are refactored/extracted to a `global.css` file and a specific `.module.css` file which are subsequently imported. If you want to read more, I suggest the [Styling section of the upgrade docs](https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#step-7-styling).

**Before**

Global styles and themes were established in a config file in the root directory.

```javascript
// themeConfig.js
export const theme = {
  colors: {
    bgDark: '#000000e6',
  },
};

export const GlobalStyles = `
  /* redacted for brevity */
`;
```

These global styles and themes are applied in the `pages/_app.tsx` file.

```tsx
// pages/_app.tsx
import { ThemeProvider } from 'styled-components'
import { GlobalStyles, theme } from '../themeConfig';

export default function App(/* redacted for brevity */) {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyles />
      {/* redacted for brevity */
    </ThemeProvider>
  );
}
```

Within  each component “CSS-in-JS” is written leveraging string interpolation to access the theme variables where desired.

```tsx
// Footer.tsx
import styled from 'styled-components';

const FooterWrapper = styled.div`
  background: ${(theme) => theme.colors.bgDark};
`;

// Usage of `<FooterWrapper />` in component render redacted for brevity
```

**After**

Global CSS and theme variables are set in an `app/global.css` file. Theme variables are set using the `:root` pseudo-class selector to make them available everywhere.

```css
/* app/global.css */
:root {
  --bg--dark: #000000e6;
}

body {
  /* redacted for brevity */
}
```

These globals are then included in the base layout of the app router. There is no need to specifically apply them via `className` or any other method. This is taken care of for us by Next.

```tsx
// app/layout.tsx
import './global.css'

// default export redacted for brevity
```

A `.module.css` file is created for specific components/pages. It is best practice to use _lowerCamelCase_ for class names as it is easier to dot-access them after they are imported into your component/page files.

```css
/* components/Footer.module.css */
.footerWrapper {
  background: var(--bg--dark);
}
```

This module specific CSS is imported into a `styles` variable and then applied where desired by specifying a `className` for whichever HTML tag or React component you want the styling to apply. Next handles unique class name generation, bundling, etc. all for us.

```tsx
// components/Footer.tsx
import styles from './Footer.module.css';

export default function Footer() => {
  return (
    <div className={styles.footerWrapper}>
      {/* redacted for simplicity */}
    </div>
  );
}
```

**Pros** – Overall, it's fine. Some things are harder/worse, sure, but I found no hills I was willing to die on.

* I used significantly less `use client;` directives to get my CSS-in-JS working.
* There's no extra system to learn, it's normal CSS.
* Next handles all the hard stuff without customization: unique class name management, bundling, etc.
* CSS modules keep the CSS that's affecting your components alongside the components themselves and not hidden away in some massive CSS file.

**Cons** – Overall, CSS variables are less powerful than systems like SASS/Less/CSS-in-JS.

* You can't use CSS variables in media queries yet, so common breakpoints must be manually kept in sync.
* You can't nest selectors in CSS, necessitating more lines of CSS to do the same thing.

After I had fully refactored _all_ components and pages to CSS modules I made sure to uninstall the no longer necessary styled-component packages and removed the `pages` directory entirely. (Note: I made sure I had a commit to roll back to just in case I missed something. You might want to do the same if you're facing a similar migration effort.)

```zsh
npm uninstall styled-components babel-plugin-styled-components @types/styled-components
```

At some point in my conversion process I found that my old way of applying my font needed modification to work with CSS modules, so I paused where I was and put another item on the top of the task stack.

#### Task 2.3.2 – Update font usage for CSS modules

**Previously** I was using a class name and importing the font into multiple files. Example 👇🏻:

```tsx
// components/Footer.tsx
import { Share_Tech_Mono } from '@next/font/google';
import styles from './Footer.module.css';

export default function Footer() => {
  return (
    <div className={`${styles.footerWrapper} ${Share_Tech_Mono.className}`}>
      {/* redacted for simplicity */}
    </div>
  );
}
```

**Now** I set a CSS variable, _once,_ in the base layout of the app router and simply use it in whatever component/page CSS module file I want 

```tsx
// app/layout.tsx
const shareTechMono = Share_Tech_Mono({
  weight: '400',
  subsets: ['latin'],
  variable: '--font--share-tech-mono',
  fallback: ['monospace'],
});

export default function BaseLayout(/* redacted for brevity */) {
  return (
    <html lang="en" className={shareTechMono.variable}>
      {/* redacted for brevity */}
    </html>
  );
};
```

```css
/* components/Footer.module.css */
.footerWrapper {
  font-family: var(--font--share-tech-mono, monospace), monospace;
}
```

The order of task stacking in this narrative and their actual order have well and truly diverged. For the sake of clarity we'll say my next discovery that made its way to the top of my stack arose during the conversion of my second page from the _pages router_ and to the _app router_.

### Task 2.4 – Refactor my second page to use the _app router_

I often find the first task I take on when refactoring to a new framework/pattern to be a relatively straightforward process. The issues only really start to rear their heads when I tackle a second task. Converting my second page was no exception. The first thing I noticed was that I had too many `page.tsx` files open in my editor and that _is a hill I'm willing to die on._ It's hands down my least favorite commonly accepted pattern in all of development. Even one `index.js` file irks me, because rarely is that file actually an index. Grrr. Anyways, I'll step away from the soapbox and digress.

#### Task 2.4.1 – Avoid `page.tsx` hell

I wish I could remember who introduced me to this pattern that alleviates my frustrations, but alas I do not remember. The strategy itself is fairly simple. Take on a little bit of extra boilerplate now, so the rest of your time working in the codebase is significantly more enjoyable. Every page of mine adopts the following pattern. `page.tsx` is pure boilerplate, and actual code is written in files that follow this pattern: `UsefulName.page.tsx`. For instance, a page accessible at `http://localhost:3000/second-page` would look something like👇🏻.

This boilerplate page:

```tsx
// app/second-page/page.tsx
import SecondPage, { metadata as pageMetadata } from './Second.page';

export const metadata = { ...pageMetadata };

export default SecondPage;
```

Which serves the contents of this file where my actual code lives:

```tsx
// app/second-page/Second.page.tsx`
export const metadata = {
  title: 'Second – piratematt.com',
};

export default function SecondPage() {
  /* redacted for brevity */
}
```

Note: I fully expect this boilerplate pattern to be rendered unnecessary by a future Next.js update and/or a configuration. I know I'm not the only developer willing to die on this hill. I spent some time digging around to see if it already existed, but I exhausted the amount I was willing to spend trying to alleviate my frustrations using “official” means and methods. Sometimes you have to know when to go with the sub-optimal solution you can do quickly and confidently over the optimal solution you don't yet know.

We’re once again departing from the actual order of events, but for the sake of the narrative let’s say this is when I came upon my next task for the top of the stack. My second page required a different layout than any of my other pages. Okay, it didn’t _need_ a different layout, but I’m using one because I want to.

#### Task 2.4.2 – Handle multiple layouts; refactor away from the [getLayout pattern](https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#per-page-layouts)

Previously I was using the [`page.getLayout` pattern](https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#per-page-layouts) to dynamically pull in a different layout for a single, on-off page. I _could have_ duplicated layouts, but adhering to DRY (don't repeat yourself) seemed the wiser path. Within the app router, I failed to find a way to repeat my _getLayout_ pattern. Instead I wound up refactoring my code structure to make use of _route groups_ and several nested `layout.tsx` files.

Oddly enough—considering my previous rant about `page.tsx` hell—I adopted a slightly different pattern to prevent `layout.tsx` hell. This is likely due to the fact that I only ever plan on having two layouts, but I knew I'd want to support `n` pages. When things were all said and done, I ended up with 3 `layout.tsx` files, 2 route groups, and 1 `CommonLayout.tsx` component.

Before I dive into example code, let's talk briefly about route groups. Simply put, _route groups_ are a [special naming convention for a folder](https://nextjs.org/docs/app/building-your-application/routing/route-groups), `(normal-layout)` for example, that enable us to logically group pages together without adding to the route's URL path. So `app/(normal-layout)/about/page.tsx` is accessible at `http://localhost:3000/about` and `app/(abnormal-layout)/second-page/page.tsx` is accessible at `http://localhost:3000/second-page`. With that out of the way, consider the following example code.

First we have the `CommonLayout` component that is imported and utilized in every `layout.tsx` file.

```tsx
// app/CommonLayout.tsx
import NavHeader from '../components/NavHeader';
import Footer from '../components/Footer';

export default function CommonLayout({
  abnormal,
  children,
}: {
  abnormal: boolean,
  children: React.ReactNode,
}) {
  return (
    <>
      <NavHeader abnormal={abnormal} />
      <div>{children}</div>
      <Footer abnormal={abnormal} />
    </>
  );
}
```

Next up we have the base layout that applies to _every_ page the app router serves. You can see it's only doing the bare minimum, leaving the specifics of the layout up to each route group.

```tsx
// app/layout.tsx
// styles, fonts, etc. redacted for brevity

export default function BaseLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <html lang="en">
      <body>
        {children}
      </body>
    </html>
  );
};
```

Then we have our two route group `layout.tsx` files, which are nearly identical, except for a single boolean switch.

```tsx
// app/(normal-layout)/layout.tsx
import CommonLayout from '../CommonLayout';

export default function NormalLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <CommonLayout abnormal={false}>
      {children}
    </CommonLayout>
  );
};
```

```tsx
// app/(abnormal-layout)/layout.tsx
import CommonLayout from '../CommonLayout';

export default function AbnormalLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <CommonLayout abnormal={true}>
      {children}
    </CommonLayout>
  );
};
```

With our two route groups now setup, our pages reside and are accessible at (note: I removed my Second.page.tsx pattern to simplify things):

* First/Home Page lives at `app/(normal-layout)/page.tsx` and is accessible at `http://localhost:3000`.
* Second Page lives at `app/(abnormal-layout)/second-page/page.tsx` and is accessible at `http://localhost:3000/second-page`.

Originally I didn't even have `app/layout.tsx`. I only had two layout files, one in each route group, but the next task we’re throwing on top of the stack made it a necessary refactor.

### Task 2.5 – Refactoring `pages/404.tsx` to `app/not-found.tsx`

The content of my 404/not-found page was a straightforward port. I simply moved the content from `pages/404.tsx` to `app/not-found.tsx`. Figuring out what level the layout needed abstracted to and how to minimize repeating myself took some trial and error. Long story short my base `app/layout.tsx` doesn't do any of the actual layout lifting. As you can see above, that's all handled by my `CommonLayout.tsx` file.

**Previously** I had something like this, leveraging the aforementioned `getLayout` pattern:

```tsx
// pages/404.tsx
import Layout, { PageType } from '../layouts/MainLayout';

const Page: PageType = function Page404 () {
  return (
    <>{/* redacted for brevity  */}</>
  );
}

Page.getLayout = function getLayout(page) {
  return (
    <Layout>{page}</Layout>
  );
}

export default Page;
```

**Now** I have:

```tsx
// app/not-found.tsx
import CommonLayout from './CommonLayout';

export default function Page404() {
  return (
    <CommonLayout abnormal={false}>
      {/* redacted for brevity */}
    </CommonLayout>
  );
}
```

### Task 2.6 – Refactoring `<Head>` components to `metadata` exports

If you have extra sharp eyes, you may have picked up on the way the _app router_ handles page metadata when I was talking through my `page.tsx` boilerplate. No longer will Next extract the `<Head>` component from a page file. Instead you simply export a named `metadata` object which Next ensures gets converted into header tags and included in the server response. If you attempt to export a metadata object from a client-rendered component/page with the `use client;` directive, you'll find Next yells at you. You can only modify page metadata server-side.

Example, what **previously** looked like this:

```tsx
// pages/about.tsx
export default function AboutPage () {
  return (
    <>
      <Head>
        <title>About - piratematt.com</title>
        <description>A wall of text covering this {'"pirate"'} thing. Complete with a {'tl;dr'}.</description>
      </Head>
      <PageWithTitle title="about">
        {/* redacted for brevity */}
      </PageWithTitle>
    </>
  );
}
```

**Now** looks like this:

```tsx
// app/about/page.tsx
export const metadata = {
  title: 'About – piratematt.com',
  description: 'A wall of text covering this "pirate" thing. Complete with a tl;dr.',
};

export default function AboutPage() {
  /* redacted for brevity */
}
```

## Take-aways & what's next for this series

I like what they're trying to do with Next.js 13 and the _app router_—be opinionated about structure to increase clarity. The necessity of the `use client;` directive to leverage state, etc. makes it crystal clear what can be rendered server-side and what requires a client. When all was said and done, I had _only a single_ component that needed the client directive. If it were up to me I'd require a `UsefulName.page.tsx` pattern instead of just `page.tsx` but I have a workaround I like well enough to continue using Next. 

At the end of the day, if you leverage [Next's upgrade guides](https://nextjs.org/docs/pages/building-your-application/upgrading) you won't run into any show-stopping issues when updating to Next.js 13—at least not for a static brochure site. I suspect you may even come to prefer the changes by the end of your efforts, as I have.

**What's next in this series?** As a part of my upgrade efforts I've taken another look at my planned entries for the remainder of the series and cut it down quite a bit. Next's documentation already does a great job covering some of the topics I had planned, and for some of the others, I simply don't use the associated tools/technologies in this project anymore. Therefore, I've decided to preserve my writing time for other efforts. As it stands now here's the latest and greatest plan:

* [x] [v3 – Overview & Getting Started](https://dev.to/piratematt/piratemattcom-v3-overview-getting-started-2ghn)
* [x] Upgrading to Next.js 13 (this entry!)
* [ ] Deploying Next.js to Github Pages
* (_considering_) Final thoughts, conclusions, and future ideas.

If you've made it this far, thanks for reading! If you have comments, critiques, cautions, or clairvoyances please feel welcome to share them!