Redirect every WordPress URL to Next.js in one hop: why Next.js sends 308s, where ?p= IDs belong, and how to test a map of thousands before launch. Code tested on Next.js 16.

Redirects are where WordPress migrations keep or lose their search traffic. One missing rule can strand a page that ranked for years; a sloppy one can bounce visitors through three hops or loop them forever.
This guide covers how Next.js redirects are processed, which tool fits each WordPress URL family, and how to prove every old URL lands in one hop before launch. It is part of our WordPress to Next.js migration guide, and every code sample below was run against Next.js 16.
permanent: true in Next.js returns 308, which Google lists as a permanent move alongside 301.The first matching rule wins, so order matters. The rewrites documentation sets it out: headers, then redirects, then Proxy (the Next.js 16 name for middleware), then beforeFiles rewrites, then static files and pages, then afterFiles rewrites, dynamic routes and finally fallback rewrites. The redirecting guide confirms that Proxy runs after next.config redirects and before rendering.
Two consequences follow. First, redirects are checked before the filesystem, including pages and /public files, so a broad pattern such as /:slug will redirect a page you meant to keep. Keep each pattern as narrow as the URL family it targets. Second, nothing in Proxy can override a config redirect that already matched.
| Tool | Status code | Best for |
|---|---|---|
| redirects() in next.config | 307 or 308 via permanent, or a custom statusCode | Pattern rules for WordPress URL families; short explicit lists |
| NextResponse.redirect in Proxy | Any | Query-string URLs, large maps, conditional logic |
| permanentRedirect() in a Server Component | 308 | Content-driven moves, such as an editor changing a slug |
| Host or CDN rules | Varies | Domain-level moves (www to apex, HTTP to HTTPS) |
Those status codes come from the Next.js redirecting guide. The first two carry almost all of a WordPress migration.
With permanent: true, Next.js responds with 308, because older clients often switched a redirected POST to GET under 301 and 302. Search engines do not mind: Google lists 301 and 308 together as permanent moves, and treats a permanent redirect as a strong signal that the target should be canonical.
If an SEO tool, a partner or a stakeholder insists on a literal 301, set statusCode: 301 on the rule instead of permanent (the docs say use one or the other, not both). There is no ranking reason to do it.
Every WordPress REST response includes a link field with the item’s permalink. Export posts, pages and media (see how to export WordPress to MDX), generate the map with Node.js, then serve each row as a literal redirect from next.config.ts. Post-name permalinks such as /post-name/ and attachment pages have no pattern, so this map is the only thing that catches them:
The media step is the one teams most often skip. WordPress creates an attachment page for many uploads, and the media endpoint returns both that page’s link and the post it was attached to: exactly the pair you need to send it home. The 1,024 limit on Vercel counts every rule, map rows and patterns together; past that, move the map into Proxy (see below).
Next.js matches source patterns with path-to-regexp. A named parameter such as :slug matches one segment, and a regex in brackets constrains it. Patterns are anchored to the start of the path, and the characters ( ) { } : * + ? must be escaped when you mean them literally. The docs add a warning worth repeating: leave out the / before a parameter’s colon and it becomes a literal string, which risks infinite redirects.

Add these after the map rows. Two notes on that config. The date-archive rule would also match a real top-level page whose path happens to be four digits, so check your inventory for collisions first. And map archives to path-based URLs rather than query strings such as /blog?category=news, which are harder to canonicalise and paginate.
WordPress permalinks end in a slash, and Next.js by default redirects slash-terminated URLs to the version without one. If your new URLs keep WordPress-style slashes, set trailingSlash: true. With that setting, URLs for files with an extension and anything under .well-known/ are left unchanged, so /wp-content/uploads/ paths and verification files keep working.
Config rules can match query values through has and missing, but query values in the request are passed through to the redirect destination. A config rule for /?p=123 would land on /blog/post-name/?p=123. Proxy lets you build a clean target, and keep campaign parameters while dropping WordPress IDs:
Add attachment IDs to the map from the media export to cover /?attachment_id= too. Sites that ran on plain permalinks may also have /index.php?p=123 URLs; add /index.php to the matcher if yours did. The new pages should carry canonical tags without the utm_ parameters, so tracked visits do not create duplicate URLs in the index.
Config redirects have hard ceilings on some platforms. On Vercel the limit is 1,024 redirects, and for more than a thousand the Next.js docs recommend a custom solution in Proxy. The documented pattern keeps the map in a key-value store (the example uses Vercel’s Global Config; Redis works too) or uses a Bloom filter. In the Bloom-filter version, Proxy checks a compact generated filter first and only asks a Route Handler to read the full JSON map when the path might match, which keeps the large file out of every request. Bloom filters return false positives, so the Route Handler must treat “not in the map” as a normal outcome.

Whichever store you use, keep two habits:
%D0%BF and %d0%bf must resolve to the same entry.noindex or robots.txt blocks that were only needed for the migration.Want a second pair of eyes on your redirect plan before launch? In a free 15-minute audit we review your site with you on a live screen-share and tell you what we would fix first.
The Node.js script follows each redirect manually, so it catches chains and loops that a simple status check misses. It expects one hop for the URL WordPress published and allows two for the other slash form. The Shell tab is the quick manual check for a single URL:
Run the script against staging, again against production within minutes of launch, and weekly for the first month.
This is the same discipline we used when we moved ProEd’s 68 pages to a new platform and a new domain; the ProEd migration case study shows the redirect plan and what happened to traffic afterwards.
Before you close this tab, pull every URL with impressions from Search Console, through the API if you have more than 1,000. If the list is longer than you expected, that is the point.
Lunova Labs builds Next.js sites and moves WordPress sites across with a tested, one-hop redirect map. Tell us about yours.
Ready when you are