redirects()
Sends an incoming path to a different URL with a 307 or 308 before the filesystem is checked.
redirects?(): Promise<Redirect[]> | Redirect[]
type Redirect = {
source: string
destination: string
permanent: boolean
basePath?: false
locale?: false
statusCode?: number
has?: RouteHas[]
missing?: RouteHas[]
}
type RouteHas = {
type: 'header' | 'cookie' | 'host' | 'query'
key?: string
value?: string
}- Added in Next.js v9.5.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$source | string | Incoming request path pattern, path-to-regexp syntax. Anchored to the start. `:slug` matches one segment, `:slug*` zero or more, `:slug+` one or more, `:slug?` zero or one; a regex may follow in parentheses. |
$destination | string | Path or absolute URL to send the client to. Named parameters from source are substituted, and unused query values from the request are passed through. |
$permanent | boolean | true sends 308 (cached forever by clients and search engines), false sends 307. Both preserve the request method, unlike 301 and 302. |
$basePathoptional | false | undefined= undefined | false stops the configured basePath being prefixed to source and destination. Only valid for external destinations. |
$localeoptional | false | undefined= undefined | false stops the locale being included when matching. |
$statusCodeoptional | number | A custom status code for older HTTP clients, used instead of permanent, never alongside it. |
$hasoptional | RouteHas[] | Every listed header, cookie, host or query condition must match for the redirect to apply. An omitted value matches any value; a named capture group makes part of the value available in destination as :name. |
$missingoptional | RouteHas[] | Every listed condition must NOT match for the redirect to apply. |
Return value
Promise<Redirect[]> | Redirect[]An array of redirect objects, checked in order against every incoming request.
Errors
Setting both permanent and statusCode on the same entry is invalid; a source that rewrites onto itself produces an infinite redirect loop in the browser.
Changelog
- PHP v13.3.0missing added.
- PHP v10.2.0has added.
- PHP v9.5.0redirects added.
Redirects run before the filesystem, so they win over app/ routes and public/ files with the same path. The address bar changes — that is the whole difference from rewrites, which keep the URL and swap the content.
When to reach for it
- Retiring a URL structure and keeping link equity with a 308.
- Pointing a vanity path at a marketing page.
- Bouncing a request to another host when a header or cookie is present.
What trips people up
Next.js uses 307 and 308 rather than 302 and 301 because many browsers downgrade the latter to GET, losing a POST body. 308 is cached by the browser forever, so testing a permanent redirect and then changing your mind leaves users stuck until they clear their cache — develop with permanent: false. Forget the slash before a colon (writing :path where you meant /:path) and the pattern is treated as a literal, which is the usual cause of infinite loops. Matching is anchored to the start and :slug covers one segment only; use :slug* for nested paths. Under the Pages Router these are not applied to client-side Link navigation unless proxy.ts matches the path.
Example
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/news/:slug',
permanent: true,
},
{
source: '/docs/:path*',
destination: '/guides/:path*',
permanent: false,
},
]
},
}
export default nextConfigWhen to use it
- Retire /old-blog/:slug in favour of /news/:slug after a rebuild
- Force logged-out visitors off /dashboard when the session cookie is missing
- Point an old marketing domain path at an external campaign URL
- Collapse several legacy locale prefixes onto one canonical path
Frequently asked questions
Why does Next.js use 307 and 308 instead of 301 and 302?
Because 307 and 308 preserve the original request method. Many browsers rewrite a 301 or 302 to GET, so a POST /v1/users could arrive at the destination as GET /v2/users. 308 is the permanent form, 307 the temporary one.
Why is my redirect looping infinitely?
Almost always a missing forward slash before the colon in a path parameter. Without it the pattern is read as a literal string and the destination re-matches the source. Check that source and destination cannot resolve to the same path.
What is the difference between a redirect and a rewrite?
A redirect returns a 307 or 308 and the browser's address bar changes to the destination. A rewrite serves the destination's content while the URL stays as the user typed it. Use a redirect when the old URL should stop existing.
More examples
Conditional redirect on a cookie or header
The first entry applies only when the source path, the query value and the cookie value all match. The second shows value capture: the named group (?<authorized>yes|true) makes the matched fragment available in the destination as :authorized. A plain value with no capture group is a test only, and does not become usable in the destination.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async redirects() {
return [
{
source: '/specific/:path*',
has: [
{ type: 'query', key: 'page', value: 'home' },
{ type: 'cookie', key: 'authorized', value: 'true' },
],
destination: '/another/:path*',
permanent: false,
},
{
source: '/',
has: [
{
type: 'header',
key: 'x-authorized',
value: '(?<authorized>yes|true)',
},
],
destination: '/home?authorized=:authorized',
permanent: false,
},
]
},
}
export default nextConfigEscaping basePath for an external destination
With basePath set, both source and destination are prefixed automatically. basePath: false opts a single entry out, which is only meaningful for an external destination — an internal path with basePath: false would resolve outside the app and 404.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
basePath: '/docs',
async redirects() {
return [
{
// becomes /docs/with-basePath -> /docs/another
source: '/with-basePath',
destination: '/another',
permanent: false,
},
{
// stays /without-basePath because basePath: false
source: '/without-basePath',
destination: 'https://example.com',
basePath: false,
permanent: false,
},
]
},
}
export default nextConfig
Discussion