Verified against Claude Code · 2026-07-23
Turn a Pages Router codebase into a phased App Router migration, not a risky rewrite
Produces a route-by-route migration plan that runs Pages Router and App Router side by side, ordered by risk, converting data-fetching methods and shared layout as it goes.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are producing a phased migration plan from the Pages Router to the App Router for an existing codebase. Pages Router (pages/) and App Router (app/) can coexist in the same Next.js project, with Next.js resolving routes across both simultaneously, so plan this as an incremental, route-by-route migration that ships on the main branch continuously — not a big-bang rewrite done on one long-lived branch that has to land all at once. CURRENT PAGES DIRECTORY pages/index.tsx, pages/blog/[slug].tsx (getStaticProps), pages/dashboard.tsx (getServerSideProps, behind auth), pages/account.tsx (getInitialProps, legacy), pages/api/checkout.ts DATA-FETCHING METHODS IN USE getStaticProps on the blog, getServerSideProps on the dashboard, getInitialProps on the legacy account page SHARED LAYOUT ELEMENTS A ThemeProvider, a global Header/Footer, and a custom <Html lang> attribute in _document.tsx TRAFFIC AND RISK PROFILE The dashboard is the highest-traffic authenticated route and directly affects billing; the blog is low-traffic and low-risk if something briefly breaks MIGRATION PLAN REQUIREMENTS Order routes by risk and traffic, not alphabetically or by file size: recommend migrating low-traffic, low-risk routes first to validate the approach against this specific codebase's quirks, and the highest-traffic or most business-critical routes last, once the pattern has actually been proven rather than assumed to work. For each data-fetching method currently in use, state its App Router equivalent and the actual mental-model shift involved, not just a mechanical rename: getStaticProps becomes an async Server Component that fetches directly, with the equivalent caching behavior expressed through fetch's own cache and revalidate options rather than a dedicated special function; getServerSideProps becomes an async Server Component with a fetch call that explicitly opts out of caching, or one that reads a dynamic API like cookies() or headers() that forces the whole route to render dynamically; getInitialProps should be flagged explicitly as needing the most rework of the three, since it historically ran on both the server and the client and has no direct one-to-one App Router equivalent at all. Move shared elements out of _app.tsx and _document.tsx into app/layout.tsx, the root layout, once, early in the plan rather than per-route later — both routers can share global styles and providers through careful setup, but running duplicate providers during the transition period is a common and specifically confusing source of bugs that's worth eliminating before it has a chance to compound across multiple migrated routes. Convert pages/api routes to app/api/.../route.ts one at a time, matching whatever order the page migration is already following, rather than treating API route conversion as one separate all-at-once phase disconnected from the pages that actually call them. Call out any Pages Router-only API still in active use — next/router's useRouter behaves differently from next/navigation's, a custom _error.tsx has no direct App Router file to move into — that needs a genuine behavioral adjustment during migration, not just a file move to a new location. OUTPUT FORMAT A phased table: Phase | Routes and files migrated | Data-fetching conversion needed | Risk notes. Then a short "do this first, regardless of phase" list specifically for the _app.tsx and _document.tsx to root layout move.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The single fact that makes this migration tractable instead of terrifying is one most teams don't realize going into it: pages/ and app/ coexist in the same Next.js project, and Next.js resolves routes across both, which means the migration can genuinely happen one route at a time on the main branch instead of on a long-lived branch that has to land all at once, and a plan that doesn't lead with this fact tends to default to the riskier big-bang framing by omission rather than by an actual decision anyone made. Mapping each data-fetching method to its App Router equivalent as a mental-model shift, rather than a mechanical rename, matters because getServerSideProps and a no-store fetch inside a Server Component aren't quite the same construct wearing different syntax — the latter is one fetch call opting out of caching, while the entire component around it defaults to static unless something else forces it dynamic, which is a genuinely different way of reasoning about what makes a given render dynamic versus cached, and a migration guide that treats this as find-and-replace will produce code that compiles but doesn't behave the way the original page did. Explicitly flagging getInitialProps as the hard case, rather than letting it slot into the same conversion pattern as the other two, matters because it's structurally different — it ran on both server and client, which has no App Router analog at all — and a migration plan that doesn't call this out by name tends to discover the problem mid-migration, after the surrounding route has already been restructured around an assumption that turns out not to hold for this one file. Ordering by risk and traffic rather than by file size or alphabetical convenience is the same practical bias that shows up in any real production migration: prove the pattern, including whatever this specific codebase's own quirks turn out to be, on something that doesn't matter much before touching the route that generates revenue or gates a paying customer's dashboard.
What you get back
Phase 1: migrate pages/blog/[slug].tsx (getStaticProps to an async Server Component with a cached fetch) — low traffic, low risk, validates the pattern against this codebase's actual conventions. Phase 2: move _app.tsx's ThemeProvider and Header/Footer into app/layout.tsx once, shared by both routers going forward — done early specifically to avoid duplicate providers during Phases 3 and 4. Phase 3: pages/dashboard.tsx (getServerSideProps to a Server Component using cookies() to force dynamic rendering, since it's behind auth) — higher risk, scheduled after the pattern is proven. Phase 4 (last, highest risk): pages/account.tsx using getInitialProps — flagged for an actual rewrite rather than a mechanical conversion, since it has no direct App Router equivalent and needs to be reasoned about from scratch.
Verified against
Claude Code Sonnet 4.6 · 2026-07-23
Cursor Cursor 2.1 · 2026-07-30
Changelog
- 2026-07-23 — Initial publish, verified against Claude Code (Sonnet 4.6) on a mixed Pages/App Router Next.js 16 project.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
