From Next.js Pages to App Router
July 20, 20243 min read
title: 'From Next.js Pages to App Router' description: 'Exploring the migration from Next.js Pages Router to App Router, with a focus on maintaining SEO and performance' date: '2024-07-20' section: blog cover_image: 'https://res.cloudinary.com/crbaucom/image/upload/v1733973952/crbaucom-images/migrating-from-pages-to-app-router-cover-image.png' tags: ['nextjs', 'typescript', 'react']
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:
// pages/blog/[slug].tsx - Old approach
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:
// app/blog/[slug]/page.tsx - New approach
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].tsx
After:
app/
layout.tsx
page.tsx
blog/
[slug]/
page.tsx
2. Layouts
Replaced _app.tsx
and _document.tsx
with a root layout:
// 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:
// 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:
// 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.