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
next.config.jsredirects array — static configuration, evaluated at the edge/server before rendering.- Middleware (
middleware.ts) — runs on every request at the edge; use when you need runtime logic. - App Router
redirect()/permanentRedirect()— called inside Server Components, Server Actions, or route handlers. - 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: trueemits 308 (not 301).permanent: falseemits 307 (not 302). Both preserve the HTTP method.- Use
:paramfor single segments and:path*for catch-all. - Conditional matching is supported via
hasandmissing— 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 state | next.config.js redirects() |
| Depends on cookie / header / geo at request time | middleware.ts |
| Depends on data fetched inside the component | App Router redirect() |
| Is a legacy Pages Router app | getServerSideProps |
| Is a permanent URL migration | next.config.js with permanent: true |
Gotchas that trip people up
- 308, not 301.
permanent: trueemits 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
matcherin the config to scope it, or you'll slow down static assets. - trailingSlash inconsistencies. If you have
trailingSlash: truein config, Next.js auto-redirects/footo/foo/. Stacking a custom redirect on top can cause a double-hop. - redirect() in Client Components.
redirect()fromnext/navigationonly works in Server Components/Actions. On the client, userouter.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에 피해를 주지 않도록 하세요. 무료 도구로 링크를 즉시 감사하세요.
How to Fix ERR_TOO_MANY_REDIRECTS: A No-BS Troubleshooting Guide
Stuck in a redirect loop? Learn how to diagnose and fix the ERR_TOO_MANY_REDIRECTS error with practical solutions for WordPress, Cloudflare, Apache, and Nginx.
Setting Up Redirects in Cloudflare: A No-Nonsense Guide for 2025
Master Cloudflare redirects with this practical guide. Learn Single Redirects, Bulk Redirects, common pitfalls, and real troubleshooting tips that actually work.
Mobile-First Redirects: How to Optimize for Core Web Vitals in 2025
Learn how redirects impact Core Web Vitals and mobile performance. Practical strategies to maintain LCP, INP, and CLS scores during redirects.