nextjs

Next.js Redirects: next.config.js, Middleware, and App Router

RC
Redirect Check Team
9 min read
Next.js Redirects: next.config.js, Middleware, and App Router

Next.js gives you four places to put a redirect, and picking the wrong one is a surprisingly common source of bugs. This guide covers all of them — next.config.js, middleware, the App Router's redirect(), and the Pages Router's getServerSideProps — and when to reach for each.

The four places redirects can live

  1. next.config.js redirects array — static configuration, evaluated at the edge/server before rendering.
  2. Middleware (middleware.ts) — runs on every request at the edge; use when you need runtime logic.
  3. App Router redirect() / permanentRedirect() — called inside Server Components, Server Actions, or route handlers.
  4. Pages Router getServerSideProps / getStaticProps — legacy pattern, but still widely deployed.

next.config.js: the first place to try

If the redirect rule doesn't depend on request state (cookies, headers, A/B flags), put it in next.config.js. It's the fastest path and requires no runtime code.

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true,
      },
      {
        source: '/docs/:path*',
        destination: '/documentation/:path*',
        permanent: true,
      },
      {
        source: '/promo',
        destination: '/sale',
        permanent: false, // -> 307 temporary
      },
    ]
  },
}

Key things:

  • permanent: true emits 308 (not 301). permanent: false emits 307 (not 302). Both preserve the HTTP method.
  • Use :param for single segments and :path* for catch-all.
  • Conditional matching is supported via has and missing — useful for host, cookie, or header-based routing without dropping to middleware.
{
  source: '/',
  has: [{ type: 'host', value: 'old.example.com' }],
  destination: 'https://example.com/',
  permanent: true,
},

Middleware: runtime logic at the edge

When the redirect depends on something only known at request time — auth cookies, geolocation, feature flags, A/B test buckets — use middleware.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US'

  if (request.nextUrl.pathname === '/' && country === 'DE') {
    return NextResponse.redirect(new URL('/de', request.url))
  }

  const token = request.cookies.get('auth')
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('redirect', request.nextUrl.pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/', '/dashboard/:path*'],
}

NextResponse.redirect() defaults to 307. To send a 308 (permanent):

return NextResponse.redirect(new URL('/new', request.url), 308)

App Router: redirect() and permanentRedirect()

Inside Server Components, Server Actions, and route handlers, use the helpers from next/navigation:

// app/profile/page.tsx
import { redirect } from 'next/navigation'
import { getCurrentUser } from '@/lib/auth'

export default async function ProfilePage() {
  const user = await getCurrentUser()
  if (!user) redirect('/login')
  return 
Welcome, {user.name}
}

redirect() throws a special error that Next.js catches, so it must be called outside of try/catch blocks (or re-thrown). Use permanentRedirect() for 308 semantics.

Pages Router: getServerSideProps

Still running on the Pages Router? Redirects go in the return value:

export async function getServerSideProps(context) {
  const user = await getUser(context.req)
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
  return { props: { user } }
}

Which one should I use?

If the redirect... Use this
Is static and doesn't depend on request statenext.config.js redirects()
Depends on cookie / header / geo at request timemiddleware.ts
Depends on data fetched inside the componentApp Router redirect()
Is a legacy Pages Router appgetServerSideProps
Is a permanent URL migrationnext.config.js with permanent: true

Gotchas that trip people up

  • 308, not 301. permanent: true emits 308. Most SEO tooling handles both, but if you're migrating from a system that used 301 and you need byte-for-byte parity, know this up front.
  • Middleware runs on everything. Always set matcher in the config to scope it, or you'll slow down static assets.
  • trailingSlash inconsistencies. If you have trailingSlash: true in config, Next.js auto-redirects /foo to /foo/. Stacking a custom redirect on top can cause a double-hop.
  • redirect() in Client Components. redirect() from next/navigation only works in Server Components/Actions. On the client, use router.push().
  • Redirect chains. Config + middleware + component can compound. After any change, verify with a redirect checker to make sure you haven't created a 3-hop chain.

TL;DR

Start with next.config.js. Move to middleware only when the rule needs request state. Use redirect() inside Server Components for data-driven redirects. And after every change, run your URLs through Redirect Check to catch chains and loops before they hit production.

今すぐリダイレクトをチェック

不適切なリダイレクトでSEOを損なわないでください。無料ツールで即座にリンクを監査しましょう。

#nextjs#tutorial#redirects
この記事をシェア: