Next.jsReactApp RouterMigrationSEODeploymentPerformance

Next.js App Router Migration Checklist (From Pages Router) + Common Pitfalls

Adrijan Omičević··14 min read
Share

# What You’ll Learn#

This guide gives you a step-by-step Next.js App Router migration plan from the Pages Router, plus a practical checklist you can run through with your team.

You’ll cover routing, data fetching, SEO metadata, deployment considerations, and troubleshooting for frequent production issues like caching surprises, redirects, and dynamic routes.

If you need a refresher on fundamentals, start with Getting started with Next.js. If your team is new to the mental model behind server-first UI, read React Server Components guide. For SEO-specific context, see Why Next.js for SEO.

# When to Migrate vs. Stay Put#

App Router is the long-term direction of Next.js, but “latest” is not always “best for your next sprint.” Use this decision matrix to avoid half-migrations that stall mid-way.

SignalMigrate to App Router nowStay on Pages Router for now
Product roadmapYou will add new sections, dashboards, or a new marketing site in the next 3 to 6 monthsOnly minor maintenance and bugfixing planned
Performance goalsYou need faster TTFB, streaming, better caching control, or to reduce client JSCurrent performance is already acceptable and stable
Data fetchingYou want server-first fetching, parallel routes, and granular cache policiesYou rely heavily on getServerSideProps patterns that are tightly coupled to page props
Team readinessYou can invest in code review guidelines and testing for server and client boundariesTeam is unfamiliar with RSC and cannot spare onboarding time
InfrastructureYou deploy on Vercel or a Node runtime that supports the features you needYou depend on a custom server setup or edge constraints that complicate parity
Risk toleranceYou can ship incrementally behind feature flags and measureYou have compliance or release constraints that make regressions expensive

🎯 Key Takeaway: Migrate when you have a business reason and bandwidth to test and observe. App Router is powerful, but it punishes “ship and pray” migrations with caching and rendering surprises.

# Step-by-Step Migration Plan (Practical Checklist)#

Step 0: Baseline and Inventory#

Before you touch code, capture a baseline so you can prove the migration helped.

Checklist

  • Record current Lighthouse scores for key templates: homepage, listing, detail page, checkout or signup.
  • Track Core Web Vitals in real users if possible. Google reports that CWV correlates with bounce and conversion, and LCP improvements are often measurable after reducing client JS.
  • Export your route list and map dependencies:
    • Static routes
    • Dynamic routes and catch-all
    • Redirects and rewrites
    • API routes
  • Identify pages using:
    • getInitialProps
    • getServerSideProps
    • getStaticProps and ISR
    • next/head
    • next/router
  • Decide if you will do a “new routes in App Router first” approach or a strict page-by-page migration.

Deliverable: a migration spreadsheet that lists every route, its data strategy, and SEO requirements.

Step 1: Upgrade Next.js Safely#

Do not migrate routers and upgrade major versions at the same time if you are behind. First, upgrade to a modern stable Next.js version while staying on Pages Router, then migrate.

Checklist

  • Update Next.js and React to supported versions for App Router.
  • Remove deprecated config and check build warnings.
  • Confirm CI builds and production deploy are stable.
  • Ensure you can run next build without runtime-only errors.

⚠️ Warning: If you are still using getInitialProps in _app, you will likely carry legacy constraints into the migration. Plan to replace it early, or isolate it to the remaining Pages Router area.

Step 2: Create the App Router Shell#

Add the app directory while keeping pages working. This is the backbone of incremental adoption.

Checklist

  • Create app/layout.tsx as the global layout.
  • Create app/page.tsx for the root route if you want to migrate the homepage early, otherwise keep it in pages.
  • Add app/not-found.tsx and app/error.tsx for consistent error UX.
  • Decide how you will handle global providers:
    • Providers that must run in the browser belong in a client component wrapper.
    • Server-only logic stays in server components.

Example layout skeleton:

TSX
// app/layout.tsx
export const metadata = {
  title: "Your Site",
  description: "Default description",
};
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Keep your layout minimal at first. You can reintroduce providers after the first route works end-to-end.

Step 3: Routing Migration (Pages to App Router)#

App Router routing is file-system based, but conventions differ.

Pages RouterApp Router
pages/index.tsxapp/page.tsx
pages/about.tsxapp/about/page.tsx
pages/blog/[slug].tsxapp/blog/[slug]/page.tsx
pages/blog/[...slug].tsxapp/blog/[...slug]/page.tsx
_app.tsxapp/layout.tsx plus optional client provider wrapper
_document.tsxhandled by app/layout.tsx for most cases
API routes in pages/apikeep as-is or move to app/api/route.ts

Checklist

  • Migrate routes one by one, starting with low-risk pages.
  • For each route, confirm:
    • URL stays identical
    • redirects still apply
    • canonical URLs remain correct
    • analytics events still fire

💡 Tip: Start with a route that has minimal data and no auth. It gives you a known-good template for layout, metadata, and navigation.

Step 4: Navigation and Params#

In App Router:

  • next/router is replaced by next/navigation.
  • Route params are passed via the params prop to page components.
  • Query string is accessed with searchParams.

Example:

TSX
// app/products/[id]/page.tsx
export default function ProductPage({
  params,
  searchParams,
}: {
  params: { id: string };
  searchParams: Record<string, string | string[] | undefined>;
}) {
  const id = params.id;
  const ref = searchParams.ref;
  return null;
}

Checklist

  • Replace useRouter from next/router with:
    • useRouter, usePathname, useSearchParams from next/navigation in client components
  • Avoid reading window.location in server components.
  • Keep navigation state in the URL when it impacts SEO or shareability.

Step 5: Data Fetching Migration#

This is where most time goes, and where most production bugs appear.

Replace getServerSideProps

Instead of returning props, fetch inside the server component. By default, fetch is cached in App Router unless you opt out.

TSX
// app/users/page.tsx
export default async function UsersPage() {
  const res = await fetch("https://api.example.com/users", { cache: "no-store" });
  const users = await res.json();
  return null;
}

Use cache: "no-store" for truly dynamic pages, similar to SSR. Use next: { revalidate: seconds } for ISR-like behavior.

Replace getStaticProps and ISR

TSX
// app/blog/page.tsx
export default async function BlogPage() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 300 },
  });
  const posts = await res.json();
  return null;
}

Replace getStaticPaths

Use generateStaticParams:

TSX
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const res = await fetch("https://api.example.com/slugs", {
    next: { revalidate: 3600 },
  });
  const slugs: string[] = await res.json();
  return slugs.map((slug) => ({ slug }));
}

Checklist

  • For every migrated route, explicitly decide caching:
    • cache: "no-store" for per-request data
    • revalidate for ISR-style pages
    • default caching only when data is truly static and safe
  • Ensure authenticated pages do not cache user-specific content.
  • Use server components for data fetching by default, then introduce client components only when needed.

⚠️ Warning: The most common App Router production incident is accidentally caching personalized content. If a page reads cookies or auth state, treat it as dynamic and verify responses are not shared across users.

Step 6: Client vs. Server Components Boundaries#

A good migration reduces client JS, but you must control where client components exist.

Checklist

  • Only add "use client" when you need:
    • state
    • effects
    • browser-only APIs
    • event handlers
  • Keep data fetching in server components and pass data down.
  • Avoid importing large UI libraries into server components if they end up forcing client boundaries.

A simple pattern for global providers:

TSX
// app/providers.tsx
"use client";
 
export default function Providers({ children }: { children: React.ReactNode }) {
  return children;
}

Then wrap it in app/layout.tsx where needed.

Step 7: SEO and Metadata Migration#

App Router uses the Metadata API, which is more structured than next/head and helps prevent duplicate tags.

SEO concernPages Router approachApp Router approach
Title and meta descriptionnext/head in pageexport const metadata or generateMetadata
Dynamic metadatacomputed in componentgenerateMetadata based on params
Canonicalmanual tagmetadata alternates.canonical
Open Graphmanual meta tagsopenGraph field
Robotsmanual meta tagrobots field

Example dynamic metadata:

TSX
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { revalidate: 600 },
  });
  const post = await res.json();
 
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: `https://example.com/blog/${params.slug}`,
    },
  };
}

Checklist

  • Recreate all critical tags:
    • title
    • meta description
    • canonical
    • robots rules for noindex pages
    • Open Graph and Twitter cards
  • Confirm pagination and filters use canonical correctly.
  • Ensure structured data remains present if you use JSON-LD.
  • Validate with Google Rich Results Test and Search Console URL Inspection.

For deeper reasoning on why this matters, see Why Next.js for SEO.

ℹ️ Note: generateMetadata can fetch data. Treat it as part of your route’s data strategy and apply the same caching rules, otherwise you can end up with stale titles while the page content updates.

Step 8: Redirects, Rewrites, and Middleware#

Most redirects can remain in next.config.js, but migrations often introduce path changes and trailing slash differences.

Checklist

  • Export and test your redirect rules in staging.
  • Validate old URLs still resolve correctly:
    • marketing campaigns
    • backlinks
    • indexed URLs
  • If you use Middleware:
    • verify it does not force dynamic behavior on otherwise static routes
    • avoid heavy computation in Middleware

Example redirect snippet:

JavaScript
// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: "/old-blog/:slug", destination: "/blog/:slug", permanent: true },
    ];
  },
};

Step 9: API Routes and Server Actions Considerations#

You can keep pages/api while migrating UI routes. Move to app/api only when it adds value.

Checklist

  • Keep stable API contracts during the UI migration.
  • If you introduce server actions, confirm:
    • CSRF considerations
    • auth checks are server-side
    • caching does not hide mutation results

If your team is still learning RSC and server-first patterns, align on conventions first. The fastest way is to use a short internal doc and link to React Server Components guide.

Step 10: Deployment and Observability#

App Router changes runtime behavior, especially around caching. You need visibility.

Checklist

  • Ensure your hosting supports required runtimes and streaming behavior.
  • In staging, test:
    • cold start performance
    • cache hit ratios if applicable
    • error pages and 404 behavior
  • Add logging around:
    • fetch failures
    • unexpected notFound cases
    • auth edge cases
  • Monitor:
    • TTFB distribution, not just averages
    • error rate
    • crawl errors in Search Console after launch

💡 Tip: Run a controlled rollout: migrate a single route group, deploy, and watch metrics for 24 to 48 hours before expanding. App Router issues are often traffic-dependent and won’t show in local testing.

# Practical Migration Checklist (Copy and Use)#

Use this as your “definition of done” for each migrated route.

AreaCheckDone criteria
RoutingURL parityRoute path matches old route and deep links work
RoutingDynamic paramsparams and searchParams are correct and typed
Data fetchingCaching policyExplicit no-store or revalidate chosen where needed
Data fetchingError handlingnotFound, error.tsx, and API failure states covered
RSC boundaryClient componentsOnly components needing browser APIs use "use client"
SEOMetadata parityTitle, description, canonical, OG tags match old behavior
SEOIndexing rulesrobots rules preserved for private or thin pages
RedirectsOld URLsAll legacy URLs redirect or serve equivalent content
PerformanceJS payloadClient bundle does not grow unexpectedly
DeploymentEnvironment parityEnv vars present, runtime compatible, build passes
QAAnalyticsPage views and key events still fire correctly
QARegression testsAt least smoke tests for top user flows

# Troubleshooting: Common Pitfalls and Fixes#

Caching Surprises#

Symptoms

  • Users see stale content after publishing.
  • One user sees another user’s personalized data.
  • A page updates only after a redeploy.

Root causes

  • Default fetch caching used unintentionally.
  • Shared cache on authenticated routes.
  • Metadata cached differently than page content.

Fix checklist

  1. 1
    For user-specific pages, use cache: "no-store" and avoid caching derived data.
  2. 2
    If content should update every N seconds, set next: { revalidate: N } on all relevant fetches.
  3. 3
    Ensure generateMetadata uses the same caching strategy as the page.
  4. 4
    Audit components that read cookies or headers and confirm the route is treated as dynamic.

Redirects and Canonicals Breaking SEO#

Symptoms

  • Search Console shows duplicate URLs or “alternate page with proper canonical tag.”
  • Organic traffic dips after migration.
  • Campaign URLs land on unexpected pages.

Root causes

  • Missing canonical tags after removing next/head.
  • Trailing slash changes.
  • Redirect rules not copied exactly.

Fix checklist

  1. 1
    Recreate canonical URLs using metadata alternates.canonical.
  2. 2
    Verify trailing slash behavior matches previous production.
  3. 3
    Diff old and new redirect lists and run automated tests on top 100 URLs.
  4. 4
    Validate HTTP status codes. Use 301 for permanent, 302 for temporary.

Dynamic Routes Not Matching or Returning 404#

Symptoms

  • Dynamic pages work locally but 404 in production.
  • Catch-all routes behave differently.
  • generateStaticParams builds but some paths are missing.

Root causes

  • Route file moved but folder structure is wrong.
  • generateStaticParams not returning all needed paths.
  • Mixed usage of pages and app creates ambiguous routing expectations.

Fix checklist

  1. 1
    Confirm folder structure: app/segment/[param]/page.tsx.
  2. 2
    Ensure generateStaticParams returns objects with correct keys.
  3. 3
    If some paths are truly dynamic, rely on runtime rendering and set caching accordingly.
  4. 4
    Add monitoring for unexpected 404 spikes post-release.

Client Component Bloat and Performance Regression#

Symptoms

  • Lighthouse JS execution time increases.
  • Large bundles after migration.
  • Hydration warnings.

Root causes

  • Too many "use client" files.
  • Importing a client-only dependency into a shared component that forces client rendering.
  • Moving global providers into client scope unnecessarily.

Fix checklist

  1. 1
    Make server components the default. Add "use client" only at leaf nodes.
  2. 2
    Split interactive widgets into isolated client components.
  3. 3
    Use dynamic imports for heavy client-only UI.
  4. 4
    Compare bundle sizes before and after and track changes per route.

Deployment Differences Between Staging and Production#

Symptoms

  • Works in preview, fails in production.
  • Edge runtime differences.
  • Build passes locally but fails in CI.

Root causes

  • Missing env vars.
  • Different Node runtime versions.
  • Hosting platform cache rules differ from local expectations.

Fix checklist

  1. 1
    Pin Node version in CI and match production runtime.
  2. 2
    Ensure env vars are present for both build and runtime where needed.
  3. 3
    Run next build in CI with the same flags as production.
  4. 4
    Test cold deploy behavior, not only hot reload.

# Key Takeaways#

  • Migrate incrementally by running Pages Router and App Router side-by-side, route by route, with clear “done” criteria.
  • Treat caching as a first-class migration task: explicitly choose no-store or revalidate for every route and metadata fetch.
  • Rebuild SEO using the Metadata API, including canonicals, robots rules, and Open Graph, then validate in Search Console.
  • Replace next/router with next/navigation, and rely on params and searchParams instead of manual URL parsing.
  • Expect most issues in production around caching, redirects, and dynamic routes, and prepare automated URL tests plus monitoring.

# Conclusion#

A successful Next.js App Router migration is less about moving files and more about rethinking data fetching, caching, and SEO as deliberate decisions per route.

If you want Samioda to review your migration plan, audit caching and SEO parity, or handle an incremental rollout with minimal risk, contact us and we’ll help you ship the App Router upgrade with measurable performance and stability gains.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.