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.