Next.js 15 Middleware: Clean Redirects for SEO Landings
Your landing goes live on Vercel. A week later Search Console shows dozens of “Page with redirect” URLs, duplicate /pricing and /pricing/ entries, and old backlinks hitting 404. You set canonical tags, built a sitemap, added JSON-LD — yet crawlers still waste budget on junk URL variants and dilute signals to the canonical page.
The issue isn’t content or Core Web Vitals. Redirects are half-configured at the CDN, and the App Router doesn’t know your legacy paths. next.config redirects help but don’t cover dynamic cases: geo prefixes, campaign slugs, or query normalization.
Middleware in Next.js 15 runs on the edge before any page render. It’s the right layer for SEO infrastructure on a landing site: one source of truth for 301/308, trailing slashes, www → apex, and post-redesign URL migrations.
Why Redirects Belong in Your SEO Stack
Google treats each URL as a document identity. Two URLs with identical content compete for indexing. Canonical hints help, but crawlers still fetch both variants, external links may point at the wrong one, and without 301s a slug change doesn’t transfer link equity.
| Scenario | Without middleware | With edge 301 |
|---|---|---|
/about/ vs /about | Duplicate candidates | Single canonical |
Old /blog/post after migration | 404, traffic loss | 301 → /articles/post |
www.example.com | Apex duplicate | 301 → example.com |
For small service landings crawl budget rarely hurts. For programmatic SEO (city or vertical pages at scale), every extra URL counts. Middleware scales: one function, sub-5 ms on Vercel Edge.
Use 301 (or 308 when POST safety matters) for permanent moves. Reserve 302/307 for temporary campaigns.
middleware.ts Basics
Place middleware.ts at the project root. Restrict the matcher so you don’t run logic on /_next assets or static files.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
const { pathname } = url;
const host = request.headers.get('host') ?? '';
if (host.startsWith('www.')) {
url.host = host.replace(/^www\./, '');
return NextResponse.redirect(url, 301);
}
if (pathname !== '/' && pathname.endsWith('/')) {
url.pathname = pathname.slice(0, -1);
return NextResponse.redirect(url, 301);
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)',
],
};
Clone nextUrl before mutating. Keep middleware, canonical URLs, sitemap entries, and <Link href> in the same format — mismatches trigger Search Console warnings.
If you need this kind of implementation — message on Telegram.
Legacy URL Migration
After a redesign, Google and backlinks still reference old paths. A static map in middleware beats bloating next.config when you have dozens of rules:
const LEGACY: Record<string, string> = {
'/services/web-development': '/services/websites',
'/portfolio': '/cases',
};
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
const target = LEGACY[url.pathname];
if (target) {
url.pathname = target;
return NextResponse.redirect(url, 301);
}
// trailing slash, www, etc.
}
For pattern-based old URLs (/blog/2024/slug → /articles/slug), apply regex after exact matches. Preserve query strings (UTMs) unless you explicitly clear url.search.
Marketing teams sometimes need /promo-winter → /pricing?ref=winter without a deploy. Vercel Edge Config can store a redirect map; use 302 for short promos and 301 in code for permanent slug changes.
Pitfalls and Verification
Avoid redirect chains (301 → 301 → 301), loops, redirecting / elsewhere, and heavy fetch on every request — TTFB and LCP suffer. Aim for one hop from any legacy URL to the final canonical.
Verify with:
curl -IL https://www.example.com/Pricing/
# Should end with a single 200 at https://example.com/pricing
In Search Console, “Page with redirect” is expected for old URLs; it’s a problem if your canonical URL lives there. Submit a sitemap of final URLs only.
Pick either middleware or next.config redirects for the same paths — not both — to prevent double hops.
Need help? Telegram → or vic.kell@ya.ru
FAQ
Middleware vs next.config redirects?
Config redirects are declarative and require rebuilds. Middleware supports conditions, regex, and Edge Config — better for SEO migrations and URL normalization.
Trailing slash: with or without?
Either works for Google; consistency matters. Match middleware, canonical, sitemap, and internal links. Next.js defaults to no trailing slash.
Does middleware hurt Core Web Vitals?
A lean edge function doesn’t move LCP or INP. Heavy per-request logic on static assets does — tighten the matcher.
Redirect differently for Googlebot?
No — that’s cloaking. Same 301 for users and bots.
Conclusion
A Next.js 15 landing isn’t SEO-complete with metadata and CWV alone. URL hygiene via edge middleware prevents duplicates, preserves equity on migrations, and quiets Search Console noise. One middleware.ts with a legacy map and slash rules — aligned with canonical and sitemap — deploys globally on Vercel in milliseconds.
Before restructuring, export old URLs from Analytics and Search Console, map 301s, validate chains with curl -IL, and refresh the sitemap. Your hero images and JSON-LD won’t matter if half your backlinks still land on 404.