nextjs

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

RC
Redirect Check Team
9 min read

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
이 글 공유하기: