redirects()

Sends an incoming path to a different URL with a 307 or 308 before the filesystem is checked.

Signature
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

ParameterTypeDescription
$sourcestringIncoming 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.
$destinationstringPath or absolute URL to send the client to. Named parameters from source are substituted, and unused query values from the request are passed through.
$permanentbooleantrue sends 308 (cached forever by clients and search engines), false sends 307. Both preserve the request method, unlike 301 and 302.
$basePathoptionalfalse | undefined= undefinedfalse stops the configured basePath being prefixed to source and destination. Only valid for external destinations.
$localeoptionalfalse | undefined= undefinedfalse stops the locale being included when matching.
$statusCodeoptionalnumberA custom status code for older HTTP clients, used instead of permanent, never alongside it.
$hasoptionalRouteHas[]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.
$missingoptionalRouteHas[]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

Example · typescript
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 nextConfig

When 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.

Example · typescript
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 nextConfig

Escaping 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.

Example · typescript
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

  • Be the first to comment on this lesson.