
From Next.js Pages to App Router
July 20, 20243 min read
Why Upgrade?
The Pages Router served me well, but I faced several challenges that the App Router solved:
SEO Issues with Dynamic Routes
In the Pages Router, handling metadata for dynamic routes required workarounds:
src/pages/blog/[slug].tsx
export const getStaticProps = async ({ params }) => {
const post = await getPost(params.slug)
return {
props: {
post,
// Metadata had to be passed as props
metadata: {
title: post.title,
description: post.description,
},
},
}
}The App Router provides a cleaner solution with built-in metadata support:
src/app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [post.coverImage],
},
}
}Server Components
The App Router's Server Components provide:
- Reduced client-side JavaScript
- Better initial page load
- Improved SEO with server-rendered content
Improved Performance
- Automatic component-level code splitting
- Streaming and Suspense support
- More efficient data fetching patterns
Migration Process
1. Project Structure
Before:
pages/
_app.tsx
_document.tsx
index.tsx
blog/
[slug].tsxAfter:
app/
layout.tsx
page.tsx
blog/
[slug]/
page.tsx2. Layouts
Replaced _app.tsx and _document.tsx with a root layout:
src/app/layout.tsx
import { type Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My Site',
default: 'My Site',
},
description: 'My personal website',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}3. Data Fetching
Before:
src/pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
const post = await getPost(params.slug)
return { props: { post } }
}
export async function getStaticPaths() {
const posts = await getAllPosts()
return {
paths: posts.map((post) => ({
params: { slug: post.slug },
})),
fallback: false,
}
}After:
src/app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return <BlogPostComponent post={post} />
}4. Client Components
Created a new pattern for interactive components:
'use client'
export function LikeButton() {
const [likes, setLikes] = useState(0)
return <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button>
}Lessons Learned
- Start Small: Begin with static pages, then migrate dynamic routes
- Test Early: Set up testing infrastructure before migrating
- Monitor Performance: Use tools like Lighthouse to verify improvements
- Keep Some Pages: You can mix Pages and App Router during migration
The App Router has solved my SEO challenges while providing a better foundation for future features.