Handling Nuxt Route Changes During Site Migration
Problem Statement
You are restructuring routes in a Nuxt 3 application during a migration and need old paths to 301 to their new locations without a client-side flash or a broken crawl. Nuxt 3 runs on the Nitro server engine, which means redirects can be declared statically as routeRules in nuxt.config or computed dynamically in Nitro server middleware. This page is part of CMS & Framework Routing Changes and covers both paths.
When to Use This Approach
Route rules suit path-shape changes within a host; host-level moves belong at the edge or web server, where they can be staged before the build ships.
- Your front end is Nuxt 3 (or Nuxt Bridge) and you control
nuxt.config. - You are renaming route segments or collapsing route groups during the migration.
- You want server-rendered 301s, not client-side
navigateToredirects that flash a page first. - Some redirects must read the incoming request (host, header, locale) and so need Nitro middleware.
- You are moving the Nuxt app to a new domain alongside the route changes.
Step-by-Step Instructions
Generate these from the mapping inventory rather than hand-maintaining them, so the rules stay true as the estate changes.
1. Declare Static Redirects with routeRules
routeRules in nuxt.config are applied by Nitro before the page component runs, so they emit a true server-side redirect. Use a redirect object with an explicit statusCode.
// nuxt.config.ts — Nitro applies these on the server, no component render
export default defineNuxtConfig({
routeRules: {
'/blog/**': { redirect: { to: '/articles/**', statusCode: 301 } },
'/promo': { redirect: { to: '/offers', statusCode: 302 } },
},
});
Keep the rules declarative and generated. routeRules is a plain object, which makes it easy to build from the mapping export at compile time and equally easy to hand-edit into something nobody can reason about. Prefer the former: a generated block that is regenerated whenever the inventory changes stays true, whereas a hand-maintained one diverges within a release or two and then quietly contradicts the server rules.
2. Preserve Path Segments with Wildcards
The ** wildcard captures the remainder of the path so a single rule can cover an entire legacy section instead of one rule per URL.
// One rule migrates the whole /docs tree to /help
routeRules: {
'/docs/**': { redirect: { to: '/help/**', statusCode: 301 } },
}
Wildcards are where route rules most often over-reach, because the shortest thing to write is also the broadest. A double-wildcard absorbs every path beneath a prefix including the prefix itself, which is usually more than intended and produces a redirect that fires for routes nobody was thinking about. Test each pattern against a shallow path, a deep path and the bare prefix, using paths taken from real logs rather than from the two examples that prompted the rule.
3. Handle Dynamic Cases with Nitro Server Middleware
When a redirect depends on the request — host during a multi-domain move, a header, or a locale cookie — use a Nitro server middleware file under server/middleware/. sendRedirect issues the response before rendering.
// server/middleware/redirect.ts — redirect requests on the old host
export default defineEventHandler((event) => {
const host = getRequestHeader(event, 'host');
if (host === 'old.example.com') {
const url = getRequestURL(event);
return sendRedirect(event, `https://www.example.com${url.pathname}${url.search}`, 301);
}
});
Reserve server middleware for decisions a rule cannot express — a lookup against a data source, a condition on request state, anything that has to run code. Everything else belongs in routeRules, partly because rules can be applied at the edge on supporting platforms and partly because a declarative rule is reviewable in a way that imperative middleware is not. The test is simple: if the redirect can be written as a mapping from path to path, it is a rule.
4. Keep Query Strings Intact
sendRedirect does not append the query string for you — read it from the request URL and pass it through, as ${url.search} above. For routeRules, the wildcard target preserves the captured path but confirm parameters survive in testing.
// Append the original query string explicitly so trackers survive
const target = `/articles${url.pathname.replace('/blog', '')}${url.search}`;
return sendRedirect(event, target, 301);
Nuxt puts its redirect configuration unusually close to the server, which is an advantage and a thing to verify rather than assume.
Confirm the deployment target’s behaviour once, early, and record it. The same routeRules block is served by a local dev process, by Nitro at an origin, or by an edge runtime depending on where the build is deployed — and on a static export it may not be applied at all. That variability is a strength when you know about it and a trap when you do not, because development will never reveal it.
Worked Example
A Nuxt 3 site moving /blog to /articles and changing host. With the routeRules wildcard plus the host middleware deployed:
$ curl -sIL https://old.example.com/blog/nuxt-migration?ref=newsletter
HTTP/1.1 301 Moved Permanently
location: https://www.example.com/articles/nuxt-migration?ref=newsletter
HTTP/1.1 200 OK
The request resolves in a single server-side 301, the ref parameter is preserved, and no client-side render flashes the old page first. Order matters: the host middleware runs first to fix the domain, and the route rename is handled by routeRules on the new host.
Wildcards in route rules are convenient and are the usual source of an over-broad match, because the syntax makes a greedy pattern the shortest thing to write.
Query strings are not preserved automatically by every routing layer, and the failure is invisible to a status-code check. A rule that maps a path correctly while dropping ?utm_source= still returns a clean 301 to a working page — it simply detaches the visit from the campaign that produced it, which surfaces days later as paid traffic appearing in reports as direct. Confirm preservation explicitly by requesting a URL with parameters attached and reading the Location header.
Verification
Check on the real deployment target rather than locally, since the layer that serves route rules differs by target and a static export may not apply them at all.
- Confirm a single server-side hop:
curl -sIL https://old.example.com/blog/nuxt-migration | grep -iE '^HTTP|^location'. - Verify no client-side redirect by checking the status is 301 in the headers, not a 200 with a JS navigation.
- Re-crawl the legacy route tree and confirm the query string survives on parameterised URLs.
FAQ
Should I use routeRules or server middleware for Nuxt redirects?
Use routeRules for static, request-independent path changes — they are declarative and easy to audit. Use Nitro server middleware only when the redirect must read the request (host, header, locale), as in a multi-domain move.
Why not redirect in the page component with navigateTo?
A component-level redirect runs after the page starts rendering, which can flash the old content and is not a clean server-side 301 for crawlers. Prefer routeRules or Nitro middleware so the redirect happens before render.
Is Nitro middleware or a routeRule the right home for a redirect? A rule wherever the decision depends only on the path, middleware only when it needs something a rule cannot see — a cookie, a header, a geographic hint. The reason to prefer rules is that they can be applied at the edge on platforms that support it, which makes them roughly as fast as a CDN redirect, while middleware always executes as code. As with any framework, the cheapest option is usually not to involve the framework at all: a bulk legacy map belongs at the web server or edge, staged before the new build ships.
What happens to route rules on a statically generated build? It depends entirely on the host, which is why this needs verifying rather than assuming. Some static hosts read a generated configuration file and apply the rules; others ignore it and serve the output directory as-is, in which case your redirects do not exist. Deploy a test rule to the real target and request it before relying on the mechanism for anything important — a redirect that silently does not exist is worse than one you knew you had to implement elsewhere.
Should redirects move out of Nuxt entirely for a domain change?
For the host-level rule, yes — it needs no application context, and handling it in the framework means every request to the old domain boots the application before being told to leave. Keep routeRules for path-shape changes within the host, where the framework genuinely has the clearest view of the route structure, and let the edge or web server answer anything that can be decided from the host alone. That split also lets the host rule be staged before the new build ships, which is exactly when you want it live.
Related
- CMS & Framework Routing Changes
- Configuring Next.js Redirects During Domain Migration
- Redirect Chain Elimination
← Back to CMS & Framework Routing Changes