I spent three hours debugging why a perfectly written blog post couldn't get indexed by Google. No errors. No console warnings. The dev server was fine. The problem? A silent redirect chain (old URL → new slug → canonical slug) that was bleeding link equity at every hop and confusing the crawler enough to give up.
The fix was under 20 lines of code. But finding what to fix required understanding something most Next.js tutorials skip entirely: redirects and SEO are deeply coupled, and the defaults Next.js ships with are not enough for production.
Here's what I learned, and how you can audit and fix this in your own app.
Why Next.js Redirects Break SEO More Than You Think
Next.js makes setting up redirects easy. Almost too easy. You drop something like this into next.config.js and move on:
// next.config.js
module.exports = {
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
]
},
}
That looks fine. And for a single hop, it mostly is. The problems start when:
- You've accumulated redirects over time and some of them chain (A → B → C)
- You have both a
next.config.jsredirect and a<meta http-equiv="refresh">somewhere in the component tree - Your canonical tags point to URLs that themselves redirect
- You migrated domains and your sitemap still lists the old URLs
Google's crawler follows up to 5 redirect hops before giving up, but it discounts PageRank at each step. A chain of three redirects can lose 15–25% of the equity that URL carried. More importantly, if your canonical URL redirects, Google may simply choose a different canonical, and that choice won't be yours.
Auditing Your Redirect Chain Before Writing Any Code
Before touching code, run an audit. The goal is to find chains, loops, and mismatches between your redirects and your canonical tags.
Step 1: Export your current redirects
If you have more than a handful, pull them into a flat array you can inspect:
// scripts/audit-redirects.js
const { execSync } = require('child_process')
const config = require('../next.config.js')
async function main() {
const redirects = await config.redirects()
const chains = []
const destinationSet = new Set(redirects.map(r => r.destination))
for (const redirect of redirects) {
if (destinationSet.has(redirect.source)) {
chains.push(`CHAIN DETECTED: ${redirect.source} → ${redirect.destination} → ...`)
}
}
if (chains.length === 0) {
console.log('✅ No redirect chains found')
} else {
console.log('⚠️ Redirect chain issues:')
chains.forEach(c => console.log(' ', c))
}
}
main()
Run it with node scripts/audit-redirects.js. It won't catch everything, but it surfaces the most common failure mode: a source URL that appears elsewhere as a destination.
Step 2: Validate your canonical tags match your final URLs
A canonical pointing to /blog/old-slug when the actual page is /blog/new-slug is one of the easiest mistakes to make during a migration. Grep your components for hardcoded canonical values and make sure they match your redirect destinations, not your sources.
Handling Redirects at the Right Layer
There are three places Next.js can redirect a user. Picking the wrong one hurts performance and SEO.
| Layer | Use For | SEO Behavior |
|---|---|---|
next.config.js |
Permanent structural changes | 301/308, fast, crawler-friendly |
Middleware (middleware.ts) |
Dynamic/conditional logic | Can be 307; be explicit about status |
Page-level (getServerSideProps) |
Auth-gated pages | 302 unless you set permanent: true
|
The mistake developers make is using middleware for everything because it's flexible. But middleware redirects are 307 (temporary) by default meaning crawlers won't transfer link equity. You have to be explicit:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-pricing') {
const url = request.nextUrl.clone()
url.pathname = '/pricing'
// Be explicit 308 is permanent redirect for non-GET methods
return NextResponse.redirect(url, { status: 308 })
}
}
If your redirect is permanent and structural, keep it in next.config.js. Only reach for middleware when you need runtime logic.
Automating Redirect Management with a Library
Once a project grows past ~20 redirects, managing them by hand in next.config.js becomes error-prone. You lose track of what redirected where, old entries never get cleaned up, and chains appear silently.
This is where a dedicated redirect management library pays off. One option I've been using is @power-seo, which wraps Next.js redirect config with validation and chain detection baked in.
Install it:
npm install @power-seo
Then update your config:
// next.config.js
const { withPowerSEO } = require('@power-seo')
module.exports = withPowerSEO({
redirects: [
{ from: '/old-blog/:slug', to: '/blog/:slug', permanent: true },
{ from: '/pricing-old', to: '/pricing', permanent: true },
],
// Throws a build error if it detects redirect chains
strict: true,
})
The strict: true flag is the part I actually care about it will fail your build if it detects a redirect chain. That's exactly the kind of guardrail that prevents the 3-hour debugging session I described at the start.
It also handles canonical injection based on your redirect map, which closes the loop between your structural redirects and your <head> metadata. You can read more about the approach in this writeup on the CCBD dev blog.
Key Takeaways
- Redirect chains are the silent killer. A → B → C loses PageRank at every hop and can cause crawlers to choose their own canonical not yours.
-
Layer matters for status codes.
next.config.jsgives you 301/308 automatically. Middleware defaults to 307. Always be explicit aboutpermanentwhen you mean it. - Your canonical tags must match your final destinations, not your redirect sources. Audit them after every migration.
- Build-time validation beats runtime debugging. Whether you use a library or write your own chain-detection script, catching redirect problems before deploy is dramatically cheaper than hunting them in production.
What's Your Setup?
How are you managing redirects at scale? I'm curious whether most teams keep them in next.config.js long-term, or eventually migrate to a CMS-driven approach or middleware. And have you ever been burned by a silent redirect chain in production?
Drop your war stories in the comments this is one of those problems where everyone has a slightly different version of the same painful lesson.
Top comments (0)