module.exports = { async redirects() { return [ { source: '/old-slug', destination: '/new-slug', permanent: true, }, ] },}For thousands of entries, do not hardcode. Use middleware or an edge function that looks up redirects from a JSON file, KV store or database. That keeps lookups fast at the edge and lets you update the map without a redeploy.
One subtle trap: if your existing WordPress URLs use trailing slashes, keep them. Changing /services/web-design/ to /services/web-design creates redirect chains for every internal link and every backlink, and Google will punish you for it while it works out what the canonical form now is. Decide on trailing-slash behaviour before you write a single route, and set trailingSlash in the Next.js config to match the old site.
Extracting metadata from WordPress
Do not use the XML export. It is lossy and painful to parse. Hit the REST API at /wp-json/wp/v2/posts and pull posts with their Yoast or Rank Math metadata, categories, tags, featured images and author fields. Write a script, run it against staging, diff the output.
For complex content models with ACF fields and custom taxonomies, WPGraphQL is usually a better fit than REST because one query fetches exactly what the page needs. Most marketing sites start on REST; sites with heavy ACF often move to GraphQL for build performance.
Rendering strategy: SSG, SSR, ISR and the CSR trap
Next.js gives you three server-rendering strategies. All of them ship fully rendered HTML, which is what search engines want. They differ on freshness, cost and infrastructure complexity.
- SSG (Static Site Generation). Pages are built at deploy time. Fastest, cheapest, best for content that changes rarely.
- ISR (Incremental Static Regeneration). Pages are cached statically but revalidated on a timer or via an API call. Users see the cached HTML while the next request quietly regenerates it in the background.
- SSR (Server-Side Rendering). Pages render on every request. Necessary for personalised content, dangerous for the crawl budget if used everywhere.
Which strategy for which page type
Mix them. There is no reason to pick one for the whole project.
| Page type | Strategy | Why | |---|---|---| | Blog posts, evergreen guides | SSG | Rarely change, benefit from being on the CDN | | Product catalogue, category pages | ISR | Change often enough to need freshness, too many to rebuild on every edit | | Homepage with featured content | ISR | Editors need updates to appear within minutes, not hours | | Logged-in dashboards, cart | SSR | Personalised, no caching benefit | | Interactive widgets inside a page | CSR (inside a rendered shell) | Fine as long as the shell is server-rendered |
The rule I will not soften: do not use client-side rendering for any page that earns organic traffic. Google's JavaScript processing is more reliable than it was, but it still introduces indexing delays and inconsistencies. If a page has to rank, Googlebot should get complete HTML on the first response.
A related concern: INP replaced FID as a Core Web Vital in March 2024. On Next.js sites, INP issues almost always come from oversized client bundles blocking the main thread. Audit your bundle after cutover and be ruthless about what actually needs to be a client component.
Canonicals, sitemaps and the headless-specific traps
Two areas where headless setups fail silently, sometimes for months before anyone notices in Search Console.
The WordPress sitemap conflict
If you go headless and leave WordPress running on a subdomain or admin URL, Yoast or Rank Math keeps generating a sitemap pointing at WordPress URLs. Meanwhile your Next.js frontend serves the same content on the canonical domain. Google finds both, gets confused about which version is authoritative, and either splits authority between them or indexes the wrong one.
Fix it at two ends. First, generate the sitemap from Next.js using the App Router's built-in sitemap.ts convention, and make sure that is the only sitemap referenced in robots.txt and submitted in Search Console. Second, block Google from the WordPress backend entirely, either with a noindex response header on the CMS origin or with a robots.txt disallow on the subdomain. You want exactly one set of canonical URLs on the internet, and they should be the ones your Next.js app serves.
metadataBase and the App Router canonical
In the App Router, canonicals live in the Metadata API:
// app/blog/[slug]/page.tsxexport async function generateMetadata({ params }) { const post = await getPost(params.slug) return { title: post.seoTitle, description: post.seoDescription, alternates: { canonical: `/blog/${post.slug}`, }, }}// app/layout.tsxexport const metadata = { metadataBase: new URL('https://example.com'),}Two things go wrong here. First, if you forget to set metadataBase in the root layout, relative URLs in Open Graph images and canonical tags never resolve into absolute URLs. Social previews break, canonical signals arrive mangled, and nobody sees it until an editor asks why LinkedIn is not pulling the right image. Second, Next.js will not write a self-referencing canonical for you. If you leave alternates.canonical off, Google is free to pick a duplicate as the canonical version, and URL parameters or trailing-slash variations will fragment authority.
Set a canonical on every route type. Blog posts, service pages, category pages, the homepage. All of them.
For teams still on the Pages Router, the fundamentals are the same but the ergonomics are worse. Do not use router.asPath for canonicals on dynamic SSG or ISR routes: at build time it puts the file pattern into the tag instead of the resolved slug. Build the canonical from the props you already have.
Structured data and internal links: preservation and upgrade
Treat the migration as a chance to fix schema, not just carry it across. WordPress schema is often incomplete: an Article without a proper author reference, breadcrumbs missing from category pages, a FAQ page with no FAQPage markup. Rebuild it properly in the Next.js layer, and use the migration audit to catch gaps that were quietly costing you rich results. This is worth doing carefully; we cover the reasoning in detail in structured data and schema for rich results and AI citation.
Internal links are the other overlooked signal. In WordPress, editors write links into rich text and the database stores them as absolute or relative paths. When you migrate content, three failure modes appear. Hardcoded links to the old WordPress domain, still pointing at the CMS after cutover. Broken anchor links because heading IDs or slugs shifted. Lost contextual links because a Markdown or block conversion stripped them.
Run a full internal link crawl the day the site goes live, and again a week later. Screaming Frog will surface broken links quickly. Fix them before Google notices and wastes crawl budget on 404s.
Migration work like this sits inside our broader web design and development practice, and the same principles apply whether the destination is Next.js, Astro or a headless commerce platform. The failure modes rhyme.
Post-launch monitoring: what to watch and for how long
Cutover is not the finish line. It is the start of a four-week window where you catch what you missed.
Watch Search Console daily for the first two weeks. Focus on the Coverage report, the Sitemaps report and Core Web Vitals. You are looking for a drop in indexed pages, a spike in crawl errors, or a jump in "Crawled, currently not indexed" status. Rankings will fluctuate; that is normal. If they have not stabilised after four weeks, something in the migration is still wrong.
A minimum monitoring stack
Screaming Frog scheduled crawl weekly, Search Console alerts on coverage drops, an uptime monitor hitting your sitemap and a handful of top landing pages, and a synthetic Lighthouse run against the ten pages that matter most. If any of these move sharply, you know within hours instead of weeks.
If rankings have not stabilised after four weeks, do not wait it out. Something in the redirect map, canonical logic or sitemap is wrong, and it will not fix itself.
Get plain-English guides like this in your inbox.
One short email a month. WordPress, Shopify, SEO, no fluff. Unsubscribe in one click.
We never share your email.
When not to migrate
The most useful answer sometimes is: not yet. If the site has no meaningful organic traffic, no clear performance ceiling that anyone has measured, and no technical SEO failure pattern nobody can solve inside WordPress, a rebuild is usually premature. WordPress with a properly configured cache, a decent host and Yoast or Rank Math will out-perform a badly built Next.js site every day of the week.
The migration makes sense when three things are true. Editors have hit a genuine wall on WordPress that a plugin cannot fix. Performance is measurably capping conversion or rankings. And the team has the capacity to maintain a JavaScript codebase, either in house or through a partner. If one of those is missing, delay.
If two of them are true and you are on the fence, our deeper decision guide, headless WordPress with Next.js: when it is worth it and when it is not, walks through the trade-offs. If you are migrating commerce as well as content, the pattern is similar but with sharper trade-offs; we wrote about that in WooCommerce to Shopify migration: an SEO-safe playbook.
If you want a second pair of eyes on the state of your current site before you commit to any of this, get a free website audit and we will tell you honestly whether a migration is the right move.
