CMS & Framework Routing Changes
Context
When you restructure routes or permalinks during a migration, the redirect logic can live in three very different places depending on the stack: a CMS plugin, a framework config file, or the web server in front of it. WordPress, Next.js, and Nuxt each expose their own redirect mechanism, and each has a server-level fallback that behaves differently for crawl budget, latency, and method preservation. Choosing the wrong layer produces redirect chains, lost query strings, or rules that silently stop firing after a deploy. This section sits under the broader URL Mapping & Redirect Architecture playbook and covers the per-stack decisions that generic server rules cannot make for you.
The teams that hit this hardest are those changing both the routing scheme and the framework at once — for example moving a WordPress blog onto a headless Next.js front end, or migrating a Nuxt 2 site to Nuxt 3 with new route rules. The timing matters: framework-native redirects ship with the application build, so they only take effect after deploy, whereas server rules can be staged ahead of cutover.
The problem this page addresses is not that frameworks redirect badly — it is that they redirect as well as everything else. A modern stack routinely has four independent layers capable of issuing a redirect for the same path: the CDN, the web server, the framework’s own routing config, and the content system’s slug handling. Each is individually reasonable, none knows about the others, and their composition is what a visitor actually experiences.
That composition is invisible in every configuration file. You can read the Nginx map, the next.config.js redirects, and the WordPress permalink settings in full and still not know how many hops a given legacy URL takes, because the answer depends on which layer answers first and what the next layer does with the result. The only reliable way to know is to request the URL and follow it, which is why every validation step on this page is a runtime check rather than a configuration review.
The remedy is to decide ownership per rule and enforce it. One layer answers for a given path; the others are configured not to. That sounds obvious and is routinely violated, usually because two teams solved the same problem in the two places each of them controls.
Pre-flight Checks
- Confirm which layer terminates TLS and sees the request first (CDN, Nginx, Apache, or the app server) — this is your lowest-latency redirect point.
- Export the complete legacy URL list and the new route scheme into a versioned map before writing any rules.
- Note the current permalink/route format exactly (trailing slashes, case, locale prefixes) so rules match byte-for-byte.
- Verify deploy cadence: framework-native redirects only go live with the next application build.
- Check whether query strings, locale prefixes, or pagination parameters must be preserved or rewritten.
- Have a staging host that mirrors production routing so rules can be tested before cutover.
Execution Steps
1. Decide the Owning Layer Per Rule
For each redirect, pick exactly one layer that owns it — server/edge, framework, or CMS plugin — and never let two layers redirect the same path. Server-level rules win on latency and can be staged before the app deploys; framework-native rules win when the redirect depends on application state. Document the choice in your map so a later deploy does not silently shadow a server rule.
Decide ownership by what the rule needs to know. A rule that depends only on the path belongs at the edge or the web server, where it is cheapest and fastest. A rule that needs to consult the database — does this slug exist, has this post moved, is this user entitled to see it — has to live in the application, because no earlier layer can answer the question. Between those, prefer the earliest layer that has enough context, and write the decision down per rule class rather than per rule.
Then enforce it by removing the alternatives. If the web server owns legacy path mapping, disable the framework’s redirect handling for those paths rather than leaving it configured and hoping it never matches. Configuration that is present but expected not to fire is a chain waiting for someone to change an unrelated rule.
2. Restructure WordPress Permalinks
If WordPress is in scope, change the permalink structure once and generate a deterministic 301 map from old to new slugs rather than relying on the database’s internal guesswork. Decide up front between the Redirection plugin (application layer) and .htaccess/Nginx rules (server layer); see migrating WordPress permalinks without losing rankings for the WP-CLI export and rule generation.
WordPress is the case where the platform actively helps and then quietly works against you. Changing the permalink structure regenerates every URL, and the built-in redirect handling does map many old paths to new ones — but it does so by database lookup on request, at application cost, and only for the patterns it recognises. Anything it does not recognise falls through to a 404, and anything it does recognise adds a hop if a server-level rule already handled it.
Export the old and new URL pairs before changing the structure, generate server-level rules from that export, and then let the platform’s own handling act only as a backstop for what the rules miss. Watch for the plugin ecosystem too: redirect plugins store their rules in the database, outside version control, and are a common source of a hop nobody can find in any config file.
3. Configure Next.js Redirects
For Next.js, prefer the static redirects() array in next.config.js for predictable path changes, and reserve middleware for redirects that need request inspection (cookies, headers, geo). The full breakdown of permanent vs temporary flags and has/missing matchers is in configuring Next.js redirects during domain migration.
Next.js draws a sharp line between redirects() in the config and logic in middleware, and the line is about what the decision depends on. Config entries are static, evaluated before the application does any work, and are the right home for anything expressible as a path transformation. Middleware runs per request with access to headers, cookies and geography, which makes it the only option for a genuinely conditional redirect — and a poor choice for a bulk legacy map, because it pays that cost on every request including the ones that never redirect.
The practical failure is putting a large static map in middleware because it was easier to express in code. It works, and it adds a per-request cost to the entire site in exchange for handling a set of paths that a server rule would have answered for free.
4. Handle Nuxt Route Changes
For Nuxt 3, route changes are expressed as routeRules in nuxt.config, executed by the Nitro server, with server middleware available for dynamic cases. See handling Nuxt route changes during site migration for the redirect rule syntax and Nitro middleware patterns.
Framework redirect configuration is evaluated in declaration order and is easy to get subtly wrong at scale, because the config is code and code invites cleverness. Keep the generated entries mechanical — one entry per mapping, sorted so exact matches precede patterns — and generate them from the same inventory that produces the server rules rather than maintaining a parallel list. A framework config and a server config describing overlapping but non-identical mappings is the single most common origin of a cross-layer chain.
Be careful with the distinction between a redirect and a rewrite. A rewrite serves different content at the same URL and issues no status code; a redirect tells the client to go elsewhere. Frameworks expose both through similar-looking configuration, and using a rewrite where a redirect was intended produces a page that works perfectly and never transfers any signal to the new URL, because as far as anything outside the server is concerned the old URL is still the live one.
5. Flatten and Validate Cross-Layer Chains
This is the step that catches what the previous four could not, because it is the only one that observes the layers working together rather than in isolation.
After wiring framework or plugin rules, re-crawl to catch chains created when an old server rule and a new framework rule both fire on one URL. Apply Redirect Chain Elimination to collapse any multi-hop sequence into a single 301, then confirm with curl -IL.
Four layers can answer a request, and the one that answers first decides what the others ever see.
Configs / Commands
Each fragment below implements the same mapping at a different layer, and they are alternatives rather than a set to apply together. Pick the one whose layer owns the rule class in question, and make sure the others are configured not to match those paths — the point of the ownership decision is lost if the losing layers are left with their rules in place.
WordPress — flush rewrite rules after a permalink change (WP-CLI):
# Re-write .htaccess / rewrite rules after changing permalink structure in the admin
wp rewrite structure '/%category%/%postname%/' --hard
wp rewrite flush --hard
Next.js — static redirects array (next.config.js):
// Static redirects evaluated at the edge before rendering
module.exports = {
async redirects() {
return [
{ source: '/blog/:slug', destination: '/articles/:slug', permanent: true }, // 308
];
},
};
Nuxt — route-level redirect (nuxt.config):
// Nitro serves these redirects without running the page component
export default defineNuxtConfig({
routeRules: {
'/blog/**': { redirect: { to: '/articles/**', statusCode: 301 } },
},
});
Server fallback — Nginx catch-all for the legacy prefix:
# Lowest-latency layer; stage this before the app deploy if possible
location ^~ /blog/ {
return 301 /articles/$uri$is_args$args;
}
When two layers both answer, the result is a chain that appears in none of the configuration files involved.
Validation
Run every check below against the deployed stack rather than against configuration, since the behaviour that matters here is the composition of several layers and no single file describes it.
- Confirm each migrated path returns a single hop:
curl -sIL https://www.example.com/blog/old-post | grep -iE '^HTTP|^location'should show one 301 then the final 200. - Check no path is redirected by two layers — a double
301in thecurl -ILtrace means server and framework rules overlap. - Verify query strings survive: request
/blog/post?utm_source=xand confirmutm_sourceappears on theLocationtarget. - After deploy, watch server logs for 404 spikes on the old route pattern indicating a missing map entry.
Nuxt’s routeRules sit closer to the server than a framework config usually does, because Nitro can apply them at the edge when deployed to a platform that supports it — which makes them genuinely fast, and also makes their deployment target something you need to confirm rather than assume. The same configuration behaves differently depending on where the build is deployed, so verify the actual response headers on the target platform rather than relying on local development, where everything is served by one process and layering questions never arise.
Rollback Triggers
- Redirect error rate on migrated paths exceeds 2% of requests over a 15-minute window — revert framework config and re-enable the prior server rules.
- Any redirect chain longer than 1 hop appears on a top-traffic path — disable the conflicting layer immediately.
- 404 rate on the legacy route prefix climbs above 0.5% of total traffic — restore the previous permalink/route scheme and regenerate the map.
- Median redirect latency exceeds 80 ms (indicating app-layer redirects that belong at the edge) — move the affected rules to the server/CDN layer.
FAQ
Should redirects live in the framework or at the web server? Put static, request-independent redirects at the server or edge for lowest latency and so they can be staged before the app deploys. Reserve framework or plugin redirects for rules that need application state such as auth, session, or feature flags.
Will framework redirects work before the new build is live? No. Redirects defined in next.config.js, nuxt.config, or a WordPress plugin only take effect once that application code is deployed. If you need cutover-day coverage, stage the equivalent rules in Nginx or Apache ahead of the deploy.
How do I stop two layers redirecting the same URL?
Assign a single owning layer per rule in your map and remove the duplicate. A double 301 in a curl -IL trace is the signature of an old server rule and a new framework rule both firing on one path.
Why does a framework redirect stop firing after a deploy?
Usually because it never fired in the first place and something else was doing the work. Framework-native redirects ship with the application build, so a rule added to next.config.js or nuxt.config does nothing until the next deploy — which means a rule that appeared to work during testing was probably being handled by a server rule, and the deploy simply changed which layer answered. The other common cause is ordering: framework configs evaluate in declaration order, and a broad entry added above a specific one silently shadows it. Confirm behaviour with curl -IL against the deployed environment rather than inferring it from the config.
Should redirects move to the server before or after the framework migration? Before, wherever the rule does not need application state. Server rules can be staged and tested ahead of the cutover, they survive the application being rebuilt or briefly unavailable, and they take effect the moment the config is reloaded rather than on the next deploy. That ordering also means the legacy paths are already handled when the new application first goes live, so the framework’s own routing is never the only thing standing between a visitor and a 404.
How do I handle locale prefixes and trailing slashes across layers? Normalise both at the earliest layer and enforce a single convention everywhere after it. These are the two details most likely to differ between a server rule and a framework’s internal routing, and a disagreement produces an endless redirect: the server strips a trailing slash, the framework adds it back, and the client gives up after twenty hops. Decide the canonical form, implement it once at the edge or web server, and configure the framework’s own trailing-slash and locale handling to agree rather than to compete.
What about redirects stored in a database by a plugin? Treat them as configuration that has escaped version control, because that is what they are. Redirect plugins store rules in the database, outside any repository, invisible to code review, and surviving deploys that would reset a config file — which makes them a frequent source of a hop nobody can locate. Export them, fold the ones worth keeping into the mapping inventory, and disable the plugin’s handling for paths the server now owns. If the plugin must stay, at least export its rules into the repository on a schedule so they are reviewable.
Related
- Migrating WordPress Permalinks Without Losing Rankings
- Configuring Next.js Redirects During Domain Migration
- Handling Nuxt Route Changes During Site Migration
- Redirect Chain Elimination
- Regex Redirect Rules
← Back to URL Mapping & Redirect Architecture