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:

// 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

  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.