The starting point was a monolith of roughly 120,000 LOC: a PHP backend on CodeIgniter 2, a middle Express layer added four years later to serve JSON endpoints, and a frontend held together with jQuery, Handlebars and bundles nobody dared touch. Eight years of patches, two aborted refactors and documentation that trailed off somewhere in 2019. Organic traffic was still decent, and that was, paradoxically, the bigger problem: every wrong move would cost real sessions.
We chose Next.js for three concrete reasons, not because it was fashionable. First, we wanted to keep SSR so we wouldn't take an SEO hit during the transition. Second, the team already wrote TypeScript on other projects and React's ecosystem was the smallest learning cost on the table. Third, the App Router let us move routes one at a time without rewriting the whole app in a single push, which was the client's non-negotiable.
We called the strategy "mirror routes" internally. We stood up the Next.js app on the same domain behind an Nginx reverse proxy, with rules that decided per route whether a request hit the old monolith or the new one. That let us release small sections (the blog first, then the product list, then checkout) without touching anything else. For months the two apps lived together in production, sharing sessions via signed cookies and a shared auth endpoint.
- Audit with Screaming Frog: we crawled 47,000 URLs and grouped them into about 180 clusters by canonical URL.
- 301 map: each old cluster pointed to a single new URL, avoiding redirect chains. We kept it in a versioned CSV and a middleware that loaded it at boot.
- Merged sitemap: we generated one sitemap.xml combining old routes (those not yet migrated) and new ones, and submitted it to Search Console before every release.
Images were their own chapter. We were serving almost 400 GB straight from an S3 bucket with no CDN in front. We moved everything to a CDN with on-the-fly transformations, rewrote <img> tags to use next/image with responsive sizes and explicit aspect ratios, and dropped roughly 12,000 orphan files along the way. Average LCP went from 4.1 s to 1.3 s.
SEO climbed 18% in the six months after the final cutover, and it wasn't magic. It was decent Core Web Vitals for the first time, structured data (Article, Product, BreadcrumbList) the monolith never had, and clean URLs without inherited query parameters. Google rewarded the obvious.
We were three people across four months: two on frontend and route migration, one on infrastructure and proxy. What we'd do differently next time: start with the sitemap and the 301s before writing a single line of Next.js, and set up visual regression tests from day one. That would have saved us two long weeks of polish at the end.