{"id":34152,"date":"2026-09-24T10:30:30","date_gmt":"2026-09-24T05:00:30","guid":{"rendered":"https:\/\/www.vocso.com\/blog\/?p=34152"},"modified":"2026-09-24T10:40:44","modified_gmt":"2026-09-24T05:10:44","slug":"nextjs-pages-router-to-app-router-migration","status":"publish","type":"post","link":"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/","title":{"rendered":"Migrating a Next.js App from the Pages Router to the App Router"},"content":{"rendered":"<div style=\"margin-top: 0px; margin-bottom: 0px;\" class=\"sharethis-inline-share-buttons\" ><\/div>\n<p>You can migrate a Next.js app from the Pages Router to the App Router one route at a time, because both routers run side by side in the same project. Move shared layout into <code>app\/layout.tsx<\/code>, rebuild each page as a Server Component that fetches its own data, then replace <code>next\/head<\/code> with the Metadata API and API routes with Route Handlers. Only migrate when the App Router gives you something you need, and never as a single big-bang rewrite.<\/p>\n\n\n\n<p>If you are still on plain React, start with our guide to <a href=\"https:\/\/www.vocso.com\/blog\/migrating-to-next-js-from-reactjs\/\">migrating from React to Next.js<\/a> and come back here afterwards.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_81 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title ez-toc-toggle\" style=\"cursor:pointer\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#should-you-migrate-at-all\" >Should you migrate at all?<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#good-reasons-to-migrate\" >Good reasons to migrate<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#when-not-to-migrate-yet\" >When not to migrate (yet)<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#incremental-migration-both-routers-can-coexist\" >Incremental migration: both routers can coexist<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#-app-and-document-become-the-root-layout\" >_app and _document become the root layout<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#mapping-data-fetching-to-server-components\" >Mapping data fetching to Server Components<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#getserversideprops-%e2%86%92-a-dynamic-server-component\" >getServerSideProps \u2192 a dynamic Server Component<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#getstaticprops-with-revalidate-%e2%86%92-cached-fetch-or-a-route-segment-option\" >getStaticProps with revalidate \u2192 cached fetch or a route segment option<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#getstaticpaths-%e2%86%92-generatestaticparams\" >getStaticPaths \u2192 generateStaticParams<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#be-explicit-about-caching\" >Be explicit about caching<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#nexthead-%e2%86%92-the-metadata-api\" >next\/head \u2192 the Metadata API<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#api-routes-%e2%86%92-route-handlers\" >API routes \u2192 Route Handlers<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#client-components-and-%e2%80%98use-client-boundaries\" >Client components and &#8216;use client&#8217; boundaries<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#data-fetching-libraries-auth-and-middleware\" >Data-fetching libraries, auth and middleware<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#common-pitfalls\" >Common pitfalls<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#context-providers-in-the-root-layout\" >Context providers in the root layout<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#third-party-libraries-that-assume-the-client\" >Third-party libraries that assume the client<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#caching-surprises\" >Caching surprises<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#leaking-server-code-to-the-client\" >Leaking server code to the client<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#a-step-by-step-migration-plan\" >A step-by-step migration plan<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#pages-router-to-app-router-checklist\" >Pages Router to App Router checklist<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#how-vocso-can-help\" >How VOCSO can help<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#frequently-asked-questions\" >Frequently asked questions<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#is-the-pages-router-deprecated\" >Is the Pages Router deprecated?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#can-i-use-the-pages-router-and-app-router-in-the-same-project\" >Can I use the Pages Router and App Router in the same project?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#will-migrating-to-the-app-router-hurt-my-seo\" >Will migrating to the App Router hurt my SEO?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#can-i-keep-using-react-query-or-swr\" >Can I keep using React Query or SWR?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-28\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#how-long-does-a-migration-take\" >How long does a migration take?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-29\" href=\"https:\/\/www.vocso.com\/blog\/nextjs-pages-router-to-app-router-migration\/#related-reading\" >Related reading<\/a><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"should-you-migrate-at-all\"><\/span>Should you migrate at all?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>The Pages Router is still supported, and a stable Pages Router app is not an emergency. Weigh the move against real problems you have today.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"good-reasons-to-migrate\"><\/span>Good reasons to migrate<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Large client bundles.<\/strong> Server Components render on the server and send no component JavaScript to the browser, which helps content-heavy pages.<\/li><li><strong>Nested layouts.<\/strong> Layouts that persist between navigations (dashboards, settings areas, docs sidebars) are native in the App Router and awkward in <code>_app<\/code>.<\/li><li><strong>Data fetching spread across a page.<\/strong> Each component can fetch what it needs instead of drilling props down from one <code>getServerSideProps<\/code>.<\/li><li><strong>Streaming.<\/strong> <code>loading.tsx<\/code> and <code>Suspense<\/code> let slow sections stream in without blocking the whole page.<\/li><li><strong>Server Actions.<\/strong> Forms and mutations can call server code directly, removing a lot of small API routes.<\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"when-not-to-migrate-yet\"><\/span>When not to migrate (yet)<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li>The app is stable, rarely changed and nearing the end of its life.<\/li><li>Critical dependencies (UI kit, CSS-in-JS library, auth, analytics) do not yet work with Server Components and you cannot replace them.<\/li><li>Your team has no time to learn the new caching and rendering model.<\/li><li>The app is almost entirely client-side (a logged-in SPA behind auth) and gains little from server rendering.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"incremental-migration-both-routers-can-coexist\"><\/span>Incremental migration: both routers can coexist<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Adding an <code>app\/<\/code> directory does not break <code>pages\/<\/code>. Next.js serves routes from both, so you can move one route, ship it, and move the next. Three rules keep this safe:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>No duplicate routes.<\/strong> The same URL cannot be defined in both directories; the build fails on the conflict. Delete the <code>pages\/<\/code> file in the same commit that adds the <code>app\/<\/code> route.<\/li><li><strong>Crossing routers is a full page load.<\/strong> Client-side navigation works within each router, but a link from a Pages route to an App route (or back) triggers a hard navigation. Migrate groups of routes that users move between often, such as a whole section, together.<\/li><li><strong>Shared components need care.<\/strong> A component that uses <code>next\/router<\/code> will not work in <code>app\/<\/code>. Keep shared UI free of router imports, or pass values in as props.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"-app-and-document-become-the-root-layout\"><\/span>_app and _document become the root layout<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>The App Router has no <code>_app<\/code> or <code>_document<\/code>. Their jobs move into <code>app\/layout.tsx<\/code>, which must render the <code>&lt;html&gt;<\/code> and <code>&lt;body&gt;<\/code> tags. Global CSS, fonts via <code>next\/font<\/code> and app-wide providers go here.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/layout.tsx\nimport type { Metadata } from 'next';\nimport { Inter } from 'next\/font\/google';\nimport Providers from '.\/providers';\nimport '.\/globals.css';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport const metadata: Metadata = {\n  title: { default: 'Acme', template: '%s | Acme' },\n  description: 'Acme product catalogue',\n};\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    &lt;html lang=\"en\" className={inter.className}&gt;\n      &lt;body&gt;\n        &lt;Providers&gt;{children}&lt;\/Providers&gt;\n      &lt;\/body&gt;\n    &lt;\/html&gt;\n  );\n}<\/code><\/pre>\n\n\n\n<p>While <code>pages\/<\/code> still exists, keep <code>_app<\/code> and <code>_document<\/code> too. They continue to wrap the Pages routes, and the root layout wraps the App routes. Shared concerns such as analytics live in both until the migration ends.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"mapping-data-fetching-to-server-components\"><\/span>Mapping data fetching to Server Components<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>This is the core of the migration. In the App Router, pages and layouts are Server Components by default and can be <code>async<\/code>, so you fetch data directly in the component. The special data functions disappear.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"getserversideprops-%e2%86%92-a-dynamic-server-component\"><\/span>getServerSideProps \u2192 a dynamic Server Component<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Fetch inside the component and opt out of caching. Reading request data with <code>cookies()<\/code> or <code>headers()<\/code> also makes a route dynamic.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/orders\/page.tsx\nimport { cookies } from 'next\/headers';\n\nexport default async function OrdersPage() {\n  const token = (await cookies()).get('session')?.value;\n  const res = await fetch('https:\/\/api.example.com\/orders', {\n    headers: { Authorization: `Bearer ${token}` },\n    cache: 'no-store',\n  });\n  const orders: Order[] = await res.json();\n  return &lt;OrderTable orders={orders} \/&gt;;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"getstaticprops-with-revalidate-%e2%86%92-cached-fetch-or-a-route-segment-option\"><\/span>getStaticProps with revalidate \u2192 cached fetch or a route segment option<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Incremental Static Regeneration becomes a <code>revalidate<\/code> value, set either per request or for the whole route. On-demand revalidation (what <code>res.revalidate()<\/code> did in an API route) becomes <code>revalidatePath()<\/code> or <code>revalidateTag()<\/code>, called from a Route Handler or Server Action.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Per request: cache and refresh at most once an hour\nconst res = await fetch('https:\/\/cms.example.com\/api\/posts', {\n  next: { revalidate: 3600, tags: ['posts'] },\n});\n\n\/\/ Or for every fetch in the route segment\nexport const revalidate = 3600;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"getstaticpaths-%e2%86%92-generatestaticparams\"><\/span>getStaticPaths \u2192 generateStaticParams<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p><code>generateStaticParams<\/code> returns the params to pre-render at build time. The <code>fallback<\/code> option maps to <code>dynamicParams<\/code>: leave it as the default <code>true<\/code> to render unknown paths on demand, or set it to <code>false<\/code> to return a 404 for them. <code>notFound: true<\/code> becomes a call to <code>notFound()<\/code>, and <code>redirect<\/code> becomes <code>redirect()<\/code>, both from <code>next\/navigation<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/blog\/[slug]\/page.tsx\nimport { notFound } from 'next\/navigation';\n\nexport const dynamicParams = true; \/\/ like fallback: 'blocking'\n\nexport async function generateStaticParams() {\n  const posts = await getAllPosts();\n  return posts.map((post) =&gt; ({ slug: post.slug }));\n}\n\nexport default async function PostPage({\n  params,\n}: {\n  params: Promise&lt;{ slug: string }&gt;;\n}) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  if (!post) notFound();\n  return &lt;Article post={post} \/&gt;;\n}<\/code><\/pre>\n\n\n\n<p>Recent Next.js versions pass <code>params<\/code> and <code>searchParams<\/code> as Promises, as above; older App Router versions passed plain objects. Check the version you are on before copying examples. If you are unsure which rendering strategy a route needs, our comparison of <a href=\"https:\/\/www.vocso.com\/blog\/static-site-generation-ssg-vs-server-side-rendering-ssr-in-nextjs-which-is-best-for-seo\/\">SSG and SSR in Next.js for SEO<\/a> covers the trade-offs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"be-explicit-about-caching\"><\/span>Be explicit about caching<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>The default caching behaviour of <code>fetch<\/code> and routes has changed between major versions, and it is the most common source of surprises. Do not rely on defaults. For every route, decide whether it is static, time-revalidated or fully dynamic, and state it with <code>cache<\/code>, <code>next.revalidate<\/code> or a route segment option. Data that does not come from <code>fetch<\/code> (an ORM or SDK call) is not cached by the fetch cache at all; wrap it with React&#8217;s <code>cache()<\/code> to deduplicate calls within one request, and use the framework&#8217;s caching APIs if you need it cached across requests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"nexthead-%e2%86%92-the-metadata-api\"><\/span>next\/head \u2192 the Metadata API<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><code>next\/head<\/code> does not work in <code>app\/<\/code>. Export a static <code>metadata<\/code> object, or an async <code>generateMetadata<\/code> function that receives the same <code>params<\/code> as the page when the title depends on data. Canonical URLs, Open Graph and robots all have typed fields, and layout metadata merges with page metadata.<\/p>\n\n\n\n<p>A fetch that <code>generateMetadata<\/code> and the page both make is memoised within the request, so you do not pay for it twice. Before you switch a high-traffic route, diff the rendered <code>&lt;head&gt;<\/code> of the old and new versions; losing a canonical tag or a robots directive is an easy SEO regression.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"api-routes-%e2%86%92-route-handlers\"><\/span>API routes \u2192 Route Handlers<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><code>pages\/api\/*<\/code> keeps working, so API routes can be moved last. When you do move them, a Route Handler is a <code>route.ts<\/code> file that exports one function per HTTP method and uses the standard Web <code>Request<\/code> and <code>Response<\/code> objects instead of Node-style <code>req<\/code> and <code>res<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/api\/newsletter\/route.ts\nimport { NextResponse } from 'next\/server';\n\nexport async function POST(request: Request) {\n  const { email } = await request.json();\n  if (!email) {\n    return NextResponse.json({ error: 'Email required' }, { status: 400 });\n  }\n  await subscribe(email);\n  return NextResponse.json({ ok: true });\n}<\/code><\/pre>\n\n\n\n<p>Before porting an endpoint, ask whether it still needs to exist. Many internal API routes only served your own forms and can become Server Actions. Keep Route Handlers for webhooks, mobile apps and other external consumers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"client-components-and-%e2%80%98use-client-boundaries\"><\/span>Client components and &#8216;use client&#8217; boundaries<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Every Pages Router component was a client component. In the App Router, anything that uses state, effects, event handlers or browser APIs needs the <code>'use client'<\/code> directive at the top of its file. That directive marks a boundary: the file and everything it imports ship to the browser.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Push the boundary down.<\/strong> Keep pages and layouts as Server Components and mark only the interactive leaves (a filter panel, a cart button) as client components.<\/li><li><strong>Pass serialisable props.<\/strong> Props crossing from server to client must be serialisable: no functions, class instances or database clients.<\/li><li><strong>Use children to nest server inside client.<\/strong> A client component cannot import a Server Component, but it can render one passed in as <code>children<\/code>.<\/li><li><strong>Swap the router hooks.<\/strong> <code>next\/router<\/code> becomes <code>next\/navigation<\/code>: <code>useRouter<\/code>, <code>usePathname<\/code> and <code>useSearchParams<\/code>. <code>router.query<\/code> and <code>router.events<\/code> are gone; read params from props or hooks instead.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"data-fetching-libraries-auth-and-middleware\"><\/span>Data-fetching libraries, auth and middleware<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p><strong>React Query and SWR<\/strong> still work, but only inside client components. Use them where you need client-side cache, polling or optimistic updates, and let Server Components handle initial page data. TanStack Query supports prefetching on the server and hydrating the client cache if you want both.<\/p>\n\n\n\n<p><strong>Authentication<\/strong> should be checked close to the data, not only in the UI. Read the session in Server Components, Route Handlers and Server Actions via <code>cookies()<\/code>, and re-check it in every Server Action, because each action is a callable endpoint. Confirm your auth library has App Router support before you start; most major ones do, but the setup differs from the Pages version.<\/p>\n\n\n\n<p><strong>Middleware<\/strong> runs before both routers, so your existing redirects, rewrites and auth gates keep working during the migration. Newer Next.js releases rename the <code>middleware.ts<\/code> file convention to <code>proxy.ts<\/code>, so check the docs for your version. Treat it as a fast first check, not your only one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"common-pitfalls\"><\/span>Common pitfalls<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"context-providers-in-the-root-layout\"><\/span>Context providers in the root layout<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Context does not work in Server Components, so a root layout cannot render <code>ThemeProvider<\/code> or <code>QueryClientProvider<\/code> directly if they are not already marked as client components. Wrap them in one client component and render that from the layout.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ app\/providers.tsx\n'use client';\n\nimport { QueryClient, QueryClientProvider } from '@tanstack\/react-query';\nimport { useState } from 'react';\n\nexport default function Providers({ children }: { children: React.ReactNode }) {\n  const [client] = useState(() =&gt; new QueryClient());\n  return &lt;QueryClientProvider client={client}&gt;{children}&lt;\/QueryClientProvider&gt;;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"third-party-libraries-that-assume-the-client\"><\/span>Third-party libraries that assume the client<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Carousels, charts, maps and older UI kits often use hooks without a <code>'use client'<\/code> directive, so they throw when imported into a Server Component. Re-export them from a small file that starts with <code>'use client'<\/code>. Runtime CSS-in-JS libraries need a style registry set up in the root layout, and some do not support Server Components at all; check this early, because it can decide your timeline.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"caching-surprises\"><\/span>Caching surprises<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Typical symptoms: stale data after a CMS update, or a route you expected to be static rendering on every request because something deep in the tree read <code>cookies()<\/code>. Check the build output, which marks each route as static or dynamic, and test revalidation against a staging CMS before launch.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"leaking-server-code-to-the-client\"><\/span>Leaking server code to the client<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>A utility that reads a secret environment variable can end up in a client bundle if a client component imports it. Add <code>import 'server-only'<\/code> to modules that must never reach the browser; the build then fails instead of leaking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"a-step-by-step-migration-plan\"><\/span>A step-by-step migration plan<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ol class=\"wp-block-list\"><li><strong>Upgrade first.<\/strong> Move to a current Next.js and React version while still on the Pages Router, fix deprecations and ship that on its own.<\/li><li><strong>Audit dependencies.<\/strong> List every library that touches rendering, styling, auth or routing and confirm its App Router support.<\/li><li><strong>Add the root layout.<\/strong> Create <code>app\/layout.tsx<\/code> with fonts, global CSS and a client <code>Providers<\/code> wrapper.<\/li><li><strong>Migrate a low-risk route.<\/strong> Pick a simple, mostly static page (About, a legal page) to prove the setup, metadata and deployment.<\/li><li><strong>Move sections, not single pages.<\/strong> Migrate groups of related routes together to avoid hard navigations between routers.<\/li><li><strong>Convert data fetching.<\/strong> Replace <code>getServerSideProps<\/code>, <code>getStaticProps<\/code> and <code>getStaticPaths<\/code>, setting caching explicitly for each route.<\/li><li><strong>Port metadata and check SEO.<\/strong> Compare titles, canonicals, structured data and status codes against the old pages.<\/li><li><strong>Replace API routes.<\/strong> Turn internal ones into Server Actions and external ones into Route Handlers.<\/li><li><strong>Remove the Pages Router.<\/strong> Delete <code>pages\/<\/code>, <code>_app<\/code> and <code>_document<\/code> once nothing depends on them, and remove unused packages.<\/li><\/ol>\n\n\n\n<p>Ship after each step so regressions are easy to trace. Your deployment pipeline does not need to change; if you host on AWS, our <a href=\"https:\/\/www.vocso.com\/blog\/deploying-a-next-js-application-on-aws-step-by-step-guide\/\">step-by-step guide to deploying Next.js on AWS<\/a> still applies.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"pages-router-to-app-router-checklist\"><\/span>Pages Router to App Router checklist<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<figure class=\"wp-block-table table table-bordered\"><table><tbody><tr><td><strong>Pages Router<\/strong><\/td><td><strong>App Router<\/strong><\/td><td><strong>Check<\/strong><\/td><\/tr><tr><td><code>pages\/_app.tsx<\/code>, <code>pages\/_document.tsx<\/code><\/td><td><code>app\/layout.tsx<\/code> with <code>&lt;html&gt;<\/code> and <code>&lt;body&gt;<\/code><\/td><td>Providers wrapped in a client component<\/td><\/tr><tr><td><code>getServerSideProps<\/code><\/td><td>Async Server Component, <code>cache: 'no-store'<\/code><\/td><td>Route shows as dynamic in the build<\/td><\/tr><tr><td><code>getStaticProps<\/code> + <code>revalidate<\/code><\/td><td><code>fetch<\/code> with <code>next.revalidate<\/code> or <code>export const revalidate<\/code><\/td><td>Content updates appear as expected<\/td><\/tr><tr><td><code>getStaticPaths<\/code> + <code>fallback<\/code><\/td><td><code>generateStaticParams<\/code> + <code>dynamicParams<\/code><\/td><td>Unknown paths render or 404 correctly<\/td><\/tr><tr><td><code>res.revalidate()<\/code><\/td><td><code>revalidatePath()<\/code> \/ <code>revalidateTag()<\/code><\/td><td>CMS webhooks still trigger updates<\/td><\/tr><tr><td><code>next\/head<\/code><\/td><td><code>metadata<\/code> \/ <code>generateMetadata<\/code><\/td><td>Titles, canonicals and robots unchanged<\/td><\/tr><tr><td><code>pages\/api\/*<\/code><\/td><td><code>app\/**\/route.ts<\/code> or Server Actions<\/td><td>Auth re-checked in each handler and action<\/td><\/tr><tr><td><code>next\/router<\/code><\/td><td><code>next\/navigation<\/code> hooks<\/td><td>No <code>router.query<\/code> or <code>router.events<\/code> left<\/td><\/tr><tr><td>Custom loading and error UI<\/td><td><code>loading.tsx<\/code>, <code>error.tsx<\/code>, <code>not-found.tsx<\/code><\/td><td>Correct HTTP status on error pages<\/td><\/tr><tr><td>Every component is a client component<\/td><td>Server by default, <code>'use client'<\/code> where needed<\/td><td>Client bundle size compared before and after<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"how-vocso-can-help\"><\/span>How VOCSO can help<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>VOCSO has been building software since 2009, and our <a href=\"https:\/\/www.vocso.com\/nextjs-development-services\">Next.js development services<\/a> team works on large, SEO-driven applications such as the <a href=\"https:\/\/www.vocso.com\/case-study\/colleges18-college-discovery-web-application-development-with-strapi-nextjs\">Colleges18 college discovery portal<\/a>, built on Next.js with a Strapi headless CMS. We can audit your Pages Router app, plan an incremental migration route by route, and protect your rankings along the way. If you need extra hands on an existing team, you can also <a href=\"https:\/\/www.vocso.com\/hire-nextjs-developers\">hire Next.js developers<\/a> from us under NDA from day one. <a href=\"https:\/\/www.vocso.com\/contact-us\">Talk to us about your migration<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"frequently-asked-questions\"><\/span>Frequently asked questions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"is-the-pages-router-deprecated\"><\/span>Is the Pages Router deprecated?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>No. It is still supported and receives fixes, but new features are designed around the App Router. New projects should start on the App Router.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"can-i-use-the-pages-router-and-app-router-in-the-same-project\"><\/span>Can I use the Pages Router and App Router in the same project?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Yes. Both directories can exist together, which is what makes incremental migration possible. The same route cannot be defined in both, and navigating between the two routers causes a full page load.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"will-migrating-to-the-app-router-hurt-my-seo\"><\/span>Will migrating to the App Router hurt my SEO?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>It should not, provided URLs, metadata, canonical tags, structured data and status codes stay the same. Compare the rendered HTML of each route before and after, and keep redirects in place for any URL that changes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"can-i-keep-using-react-query-or-swr\"><\/span>Can I keep using React Query or SWR?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>Yes, inside client components. Many teams move initial page data to Server Components and keep React Query or SWR for interactive, frequently refreshed data.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"how-long-does-a-migration-take\"><\/span>How long does a migration take?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p>It depends on the number of routes, how data fetching is organised and how many client-only dependencies you use. A dependency audit and one migrated section give a realistic estimate.<\/p>\n\n\n<!-- vocso-related -->\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"related-reading\"><\/span>Related reading<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-core-web-vitals-performance\/\">Next.js Performance: How to Pass Core Web Vitals (LCP, INP, CLS)<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/nextjs-seo-checklist\/\">Next.js SEO: A Technical Checklist for the App Router<\/a><\/li><li><a href=\"https:\/\/www.vocso.com\/blog\/static-site-generation-ssg-vs-server-side-rendering-ssr-in-nextjs-which-is-best-for-seo\/\">Static Site Generation (SSG) vs Server-Side Rendering (SSR) in NextJS: Which is Best for SEO?<\/a><\/li><\/ul>\n\n<!-- \/vocso-related -->","protected":false},"excerpt":{"rendered":"<p>A practical guide for teams with a production Pages Router app: when to migrate, how to run both routers side by side, and how each Pages Router feature maps to the App Router.<\/p>\n","protected":false},"author":23,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1415],"tags":[],"class_list":["post-34152","post","type-post","status-publish","format-standard","hentry","category-nextjs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34152","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/users\/23"}],"replies":[{"embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/comments?post=34152"}],"version-history":[{"count":2,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34152\/revisions"}],"predecessor-version":[{"id":34309,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/posts\/34152\/revisions\/34309"}],"wp:attachment":[{"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/media?parent=34152"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/categories?post=34152"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.vocso.com\/blog\/wp-json\/wp\/v2\/tags?post=34152"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}