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.

Nuxt redirect path through Nitro Request enters Nitro, which applies server middleware first then routeRules, issuing a server-side 301 before the page component renders. Nuxt Redirect via Nitro (Server-Side) Request server middleware routeRules 301 old host/path host / locale path rename new URL Redirect issued before the page component renders — no client flash
Nitro applies server middleware then routeRules, emitting a true server-side redirect.

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 navigateTo redirects 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.

Where Nuxt routeRules actually execute Route rules compared across local development, a Node server deployment and an edge deployment, showing that the same configuration is served by different layers depending on the target. The same config runs in a different place per target Deployment target routeRules served by Implication nuxt dev one local process layering never appears Node server Nitro at the origin one origin hop Edge platform the edge runtime as fast as a CDN rule Static generate whatever hosts the output may not run at all The last row is the trap: a statically generated build may drop route rules entirely depending on the host.
Verify the response headers on the real target rather than in development, where a single process hides every layering question.

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.

Wildcard scope in route rules Three wildcard patterns compared by what they match, what they capture, and the paths they unintentionally swallow. The shortest pattern is almost always too broad Pattern Intended Also matches /blog/** every post under /blog /blog itself, and deeper trees /blog/* one level of posts nothing deeper — often too narrow /blog/**:slug posts, capturing the path the same breadth, but usable Decide breadth deliberately and test against real logged paths, not against the two examples you had in mind.
The middle row is the one people reach for and then quietly widen when a deeper path 404s — at which point it is the top row again.

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

← Back to CMS & Framework Routing Changes