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].tsx1export const getStaticProps = async ({ params }) => {2 const post = await getPost(params.slug)3 return {4 props: {5 post,6 // Metadata had to be passed as props7 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.tsx1export async function generateMetadata({ params }) {2 const post = await getPost(params.slug)34 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.tsx1import { type Metadata } from 'next'23export const metadata: Metadata = {4 title: {5 template: '%s | My Site',6 default: 'My Site',7 },8 description: 'My personal website',9}1011export default function RootLayout({12 children,13}: {14 children: React.ReactNode15}) {16 return (17 <html lang="en">18 <body>{children}</body>19 </html>20 )21}
3. Data Fetching
Before:
src/pages/blog/[slug].tsx1export async function getStaticProps({ params }) {2 const post = await getPost(params.slug)3 return { props: { post } }4}56export 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.tsx1export async function generateStaticParams() {2 const posts = await getAllPosts()3 return posts.map((post) => ({4 slug: post.slug,5 }))6}78export 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'23export function LikeButton() {4 const [likes, setLikes] = useState(0)5 return <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button>6}
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.