2023-05-24

Next.js 13 App Router migration

Next.js App Router is stable since 13.4.

Here are some steps I've done to migrate my static blog from pages/ to app/ directory.

App Router Incremental Adoption Guide

Update next from package.json and package-lock.json

npm update --save next
 
# or
npm install next@latest react@latest react-dom@latest

Folder structure

# before
/pages
├── _app.tsx
├── _document.tsx
├── index.tsx
└── posts
    └── [slug].tsx
 
# after
/app
├── index-page.tsx (Client)
├── layout.tsx (Server)
├── page.tsx (Server)
└── posts
    └── [slug]
        ├── page.tsx (Server)
        └── post-page.tsx (Client)
 

app/layout.tsx (Server Component)

import "../styles/globals.css"
import { Navbar } from "../components/Navbar"
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Navbar />
        {children}
      </body>
    </html>
  )
}

app/page.tsx (Server Component)

// Import your Client Component
import IndexPage from "./index-page"
 
async function getPosts() {
  const res = await fetch("https://.../posts")
  const posts = await res.json()
  return posts
}
 
export default async function Page() {
  // Fetch data directly in a Server Component
  const posts = await getPosts()
  // Forward fetched data to your Client Component
  return <IndexPage posts={posts} />
}

app/index-page.tsx (Client Component)

"use client";
 
export default function IndexPage({ posts }) {
  return (
    <div>
      {posts.map((post) => (
        <p>{post.title}<p>
      ))}
    </div>
  );
}

Note: If your fetch fails like below in local development, change localhost to 127.0.0.1 in your fetch url.

https://github.com/node-fetch/node-fetch/issues/1624

Unhandled Runtime Error
Error: fetch failed
 
Call Stack
Object.fetch
node:internal/deps/undici/undici (11413:11)
process.processTicksAndRejections
node:internal/process/task_queues (95:5)

getStaticPaths -> generateStaticParams

app/posts/[slug]/page.tsx (Server Component)

// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
  const res = await fetch("https://.../posts")
  const posts = await res.json()
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}
 
// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default function Page({ params }) {
  const { slug } = params
  // ...
}

getStaticProps -> getProjects (can be named anything)

app/page.tsx (Server Component) or app/posts/[slug]/page.tsx (Server Component)

// This function can be named anything
async function getProjects() {
  const res = await fetch(`https://...`)
  const projects = await res.json()
 
  return projects
}
 
export default async function Index() {
  const projects = await getProjects()
 
  return projects.map((project) => <div>{project.name}</div>)
}

next.config.js

const nextConfig = {
  output: "export",
}

package.json

 
// before
{
  "build": "next build && next export",
}
 
// after
{
  "build": "next build",
}

Change Cloudflare Pages Build configuration to above.

"next build",

tailwind.config.js

// before
module.exports = {
  content: ["./pages/**/*.{js,ts,jsx,tsx}"],
}
 
// after
module.exports = {
  content: ["./app/**/*.{js,ts,jsx,tsx}"],
}

Done.