Configuring Next.js Redirects During Domain Migration
Problem Statement
You are migrating a Next.js application to a new domain or route scheme and need the old paths to 301 to their new homes. Next.js gives you two native mechanisms — the static redirects() array in next.config.js and dynamic redirects from middleware — and choosing wrong leaves you with stale 308s or redirects that cannot inspect the request. This page sits under CMS & Framework Routing Changes and covers when to use each.
When to Use This Approach
- Your front end is Next.js (App Router or Pages Router) and you control
next.config.js. - The path changes are mostly static and known at build time.
- You need wildcard or named-parameter source matching (
:slug,:path*). - Some redirects must depend on request state — cookies, headers, or geo — which forces middleware.
- You are also moving domains and want the redirect to ship with the application build.
Step-by-Step Instructions
1. Define Static Redirects in next.config.js
The redirects() async function returns an array of rules evaluated at the edge before rendering. permanent: true emits a 308; permanent: false emits a 307. Use these for predictable path changes.
// next.config.js — static rules, evaluated before the page renders
module.exports = {
async redirects() {
return [
{ source: '/blog/:slug', destination: '/articles/:slug', permanent: true }, // 308 keeps method
{ source: '/promo', destination: '/offers', permanent: false }, // 307 temporary
];
},
};
2. Match Path Segments with Patterns
Use :name for a single segment and :name* to capture the rest of the path. Wildcards let one rule cover an entire legacy prefix instead of listing every URL.
// Capture an entire legacy section in one rule
{ source: '/docs/:path*', destination: '/help/:path*', permanent: true }
3. Add Conditional Rules with has and missing
The has and missing arrays gate a redirect on a header, cookie, query param, or host. This is how you scope a redirect to one domain during a multi-host migration.
// Only redirect requests arriving on the OLD host
{
source: '/:path*',
has: [{ type: 'host', value: 'old.example.com' }],
destination: 'https://www.example.com/:path*',
permanent: true,
}
4. Use Middleware Only for Request-Dependent Redirects
When a redirect must read something redirects() cannot — a session cookie, A/B bucket, or geo header — handle it in middleware. Keep static redirects in config so they stay cacheable.
// middleware.ts — redirect logged-in users away from the legacy login path
import { NextResponse } from 'next/server';
export function middleware(request) {
if (request.cookies.get('session') && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url), 308);
}
return NextResponse.next();
}
Worked Example
A site moving old.example.com to www.example.com with a /blog → /articles rename. With the host-gated wildcard plus the slug rule deployed:
$ curl -sIL https://old.example.com/blog/dns-cutover
HTTP/2 308
location: https://www.example.com/articles/dns-cutover
HTTP/2 200
A single 308 carries the request to the new host and new path together, preserving the method and avoiding a host-then-path two-hop chain. Note that permanent: true produces 308, not 301 — confirm your analytics and search reporting treat them equivalently, which they do for ranking purposes.
Verification
- Confirm the status and final target:
curl -sIL https://old.example.com/blog/dns-cutover | grep -iE '^HTTP|^location'. - Check no chain forms when both a host rule and a path rule apply — the trace should show one 308, not two.
- After deploy, query a few middleware-gated paths with and without the cookie to confirm the conditional fires only when expected.
FAQ
Why does permanent: true return 308 instead of 301? Next.js intentionally emits 308 for permanent redirects so the HTTP method and body are preserved. Search engines treat 308 like 301 for ranking; see 301 vs 302 decision trees if you specifically need a 301.
When should I use middleware instead of redirects()?
Use middleware only when the redirect depends on request state the config cannot see — cookies, headers, geo, or auth. Keep everything static in redirects() so it stays cacheable and easy to audit.
Related
← Back to CMS & Framework Routing Changes