The application worked. Editors published stories, readers read them, and nothing was on fire. For a high-traffic newsroom run by a global apparel brand, that's no small thing. Millions of people pass through a site like this, and "it works" is the baseline everyone expects.
Under the surface, though, every new feature request came with a quiet question none of us wanted to answer out loud: which era of the framework are we actually building in? The site ran on an older major version of Next.js, still on the Pages Router, deployed through infrastructure that had been shaped by hand over a few years. It all held together. But Next.js has changed a great deal in a short time, and each request to add something meant deciding whether to build it in the patterns of two years ago or start pulling the app toward the present.
Where the mission came from
The developers on the app were the ones who kept pushing for this. They could see the version drift up close, and they were the first to say it needed real time in a sprint rather than another quarter of good intentions. We had set it aside once already to finish a larger media project the client needed first.
What moved it to the top was pressure the whole team could feel. For a brand this size, a security flag carries the weight of the whole company. Findings tied to the framework version drew a hard push from the security team, the kind that can put a client's application on the wrong list across the entire organization if it lingers. At the same time, the client had a long queue of user-facing work they were excited about and needed shipped. Both mattered, and both wanted the same sprints.
So our job was to advocate for the upgrade and actually get it moving, without stalling the exciting work the client cared about. We made the case, carved out room in the sprints, and set the constraints the upgrade could not cross: move the app to a current version of Next.js, modernize how it deploys, and keep a live site that people rely on every day running the whole time.
Part of that was new ground for us on this particular app, especially the move to a current serverless deployment model, and we want to be honest about that rather than pretend it was routine.
The starting point
Before we began, three things defined the situation. The framework was a major version behind, and the wider ecosystem had already moved on again, as the security scanner kept reminding us. The deployment was tied closely to a hand-built content delivery setup, so any change there carried real risk to uptime and to URLs we could not afford to break. And the team had no quick way to preview a single piece of the interface on its own. A button, a headline block, or a card could only be checked by loading the whole running site, which made every small design change more uncertain than it needed to be.
Upgrading without stopping the newsroom
An upgrade like this is easy to keep putting off. The app was not ancient. It was about a major version behind, recent enough that nothing looked broken and old enough that the cost was starting to show up in security scans and in the friction of every new feature. When nothing is visibly wrong, the upgrade always loses to the request with a deadline attached. That is how an app drifts away from current, one reasonable deferral at a time.
What made this one demanding was the setting. A newsroom with a large public presence does not get to go quiet while engineers work, and a busy client cannot put their feature requests and daily publishing on hold for an infrastructure project. So we did it in place and alongside the everyday work. Publishing kept going, the smaller requests kept shipping, and the upgrade moved in parallel rather than blocking the queue. Because the site is so public, careful was the only acceptable speed.
To keep the upgrade from accidentally touching live work, we stood up a separate development environment, apart from staging and production. That gave the team a dedicated place to test the migration and check quality without interfering with the site editors publish to or the staging environment the client uses for their own reviews.
The clearest example was the infrastructure. Rather than tearing down the existing content delivery setup and rebuilding it, we carried its state into the new deployment model, keeping the live site and its URLs intact instead of rebuilding them from scratch and hoping everything lined up. On a site this visible, a broken link or an hour of downtime becomes a headline.
The centerpiece: making a huge change reviewable
This is the part worth dwelling on, because it is the difference between a migration that lands and one that drags on for months.
A change this size produces an enormous diff, most of it generated files that no human should read line by line. Dropped on a reviewer as one wall of changes, it either gets rubber-stamped or it sits untouched for weeks. Neither is acceptable when the thing you are changing is a live newsroom.
So we made the review itself part of the deliverable. The pull request came with a short set of commands that let a reviewer separate the diff by concern and look at each piece on its own: the framework upgrade here, the deployment change there, and the new component previews separately. A reviewer could actually reason about each decision instead of drowning in generated output.
Just as important, we didn't let the migration live in one person's head. The engineer who built it handed it off to another engineer to finish, own, and land. That handoff made the work legible to someone other than its author, which is exactly the property you want in a change that touches the whole application.
Where it landed
The app now runs on a current version of Next.js with a modern deployment model behind it. The team can now preview each piece of the interface on its own, without loading the whole site, through Storybook wired into the pipeline, so design changes are far less nerve-wracking. The security findings tied to the framework version cleared. And the app is close enough to the present day that the next feature request no longer reopens the "which era are we in" question.
The quiet proof came after we closed the work. Two weeks passed with no reported issues on a site that serves millions of readers across every language it publishes in. For a change that touched this much of the application, a boring two weeks is the best outcome you can ask for.
The lesson
"It still works" only ever describes today. An application can run fine right now and still be drifting away from the tooling, the security patches, and the patterns that keep it cheap to change tomorrow. The fix is rarely dramatic. It means staying close to current and upgrading while the app keeps running, instead of waiting until the gap is wide enough to make everyone nervous. That is almost always the right call when the app already exists and already matters.
If your Next.js app still works but you have started to feel that quiet uncertainty about which version you are really building on, that feeling is worth listening to. It is usually cheaper to answer it now than to keep building on an answer you are no longer sure of.