From Next.js Pages to App Router

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:

src/pages/blog/[slug].tsx
1export const getStaticProps = async ({ params }) => {
2 const post = await getPost(params.slug)
3 return {
4 props: {
5 post,
6 // Metadata had to be passed as props
7 metadata: {
8 title: post.title,
9 description: post.description,
10 },
11 },
12 }
13}

The App Router provides a cleaner solution with built-in metadata support:

src/app/blog/[slug]/page.tsx
1export async function generateMetadata({ params }) {
2 const post = await getPost(params.slug)
3
4 return {
5 title: post.title,
6 description: post.description,
7 openGraph: {
8 title: post.title,
9 description: post.description,
10 images: [post.coverImage],
11 },
12 }
13}

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:

src/app/layout.tsx
1import { type Metadata } from 'next'
2
3export const metadata: Metadata = {
4 title: {
5 template: '%s | My Site',
6 default: 'My Site',
7 },
8 description: 'My personal website',
9}
10
11export default function RootLayout({
12 children,
13}: {
14 children: React.ReactNode
15}) {
16 return (
17 <html lang="en">
18 <body>{children}</body>
19 </html>
20 )
21}

3. Data Fetching

Before:

src/pages/blog/[slug].tsx
1export async function getStaticProps({ params }) {
2 const post = await getPost(params.slug)
3 return { props: { post } }
4}
5
6export async function getStaticPaths() {
7 const posts = await getAllPosts()
8 return {
9 paths: posts.map((post) => ({
10 params: { slug: post.slug },
11 })),
12 fallback: false,
13 }
14}

After:

src/app/blog/[slug]/page.tsx
1export async function generateStaticParams() {
2 const posts = await getAllPosts()
3 return posts.map((post) => ({
4 slug: post.slug,
5 }))
6}
7
8export default async function BlogPost({ params }) {
9 const post = await getPost(params.slug)
10 return <BlogPostComponent post={post} />
11}

4. Client Components

Created a new pattern for interactive components:

1'use client'
2
3export function LikeButton() {
4 const [likes, setLikes] = useState(0)
5 return <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button>
6}

Lessons Learned

  1. Start Small: Begin with static pages, then migrate dynamic routes
  2. Test Early: Set up testing infrastructure before migrating
  3. Monitor Performance: Use tools like Lighthouse to verify improvements
  4. 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.