Hi! Send me a message
Contact

Mexico City, Mexico
Available for remote and on-site projects.

Professional networks

LinkedIn · GitHub · Email
Applied engineering, measurable results.

Migrating WordPress to React and Next.js without losing SEO: best practices for enterprise migrations

· 9 min read · Samuel Torres

Lissajous curve 3:4
x(t) = sin(3t + pi/2), y(t) = sin(4t)

In short

  • SEO is lost in the URLs, not the framework: inventory every one of them before writing code.
  • Every old URL needs a destination: a 301 to its equivalent, never to the homepage.
  • Title, description, canonical and hreflang are generated on the server with generateMetadata.
  • Indexable pages render static or with ISR; the client only adds interaction.
  • Compare the raw HTML before and after, URL by URL, before switching DNS.

Migrating WordPress to React without losing SEO is, above all, an inventory problem. The framework matters less than it seems. What sinks organic traffic is URLs that stop existing, titles that repeat, and content that only shows up once JavaScript runs.

I work on enterprise migrations to React that have to preserve SEO. In this article I share the process I follow with Next.js and the App Router: what gets decided before writing code, what gets configured in the framework, and how to verify nothing was lost before switching DNS.

The examples are illustrative. They show the pattern with sample domains and routes, not any client's code.

Before the code: the URL inventory

A WordPress site accumulates years of URLs: posts, pages, categories, tags, date archives, attachments and pagination. Many of them get traffic or external links even if nobody remembers them.

If one of those URLs returns a 404 after the migration, its ranking is gone. That is why the first deliverable is not a component. It is a complete list of what exists today.

The sources for building it are combined, because none is complete on its own:

  • The current site's sitemaps. WordPress generates one per content type. They are the starting point.
  • Search Console. The pages report shows URLs with impressions that are sometimes missing from the sitemap.
  • Server logs. They reveal old URLs that still get visits or links from other sites.
  • A full crawl. It finds what is linked internally even if it is in no sitemap.

The first step can be automated in a few lines. This script extracts every URL from the WordPress sitemaps into a flat file:

#!/usr/bin/env bash
# Inventory: every URL WordPress declares today, one per line.
SITE="https://www.example.com"
: > current-urls.txt

# The index points to one sitemap per content type (posts, pages, categories…).
for map in $(curl -s "$SITE/wp-sitemap.xml" | grep -o '<loc>[^<]*' | sed 's/<loc>//'); do
  curl -s "$map" | grep -o '<loc>[^<]*' | sed 's/<loc>//' >> current-urls.txt
done

sort -u current-urls.txt -o current-urls.txt   # no duplicates: each URL is a case
wc -l current-urls.txt

With the list in hand, every URL gets a decision: keep it as is, redirect it to its equivalent, or retire it on purpose. There is no fourth option. A URL without a decision is a URL that will be lost.

Keeping URLs identical is always the safest option. If the new structure allows it, replicate the WordPress slugs and save yourself most of the redirects.

Next.js best practices for enterprise migrations

301 redirects: a map, not a generic rule

URLs that change need a permanent redirect to their exact equivalent. Redirecting everything to the homepage is almost as bad as a 404: search engines treat it as lost content.

For a fixed map, the cleanest option in Next.js is the redirects function in the config. It is resolved before rendering and does not depend on any component:

// next.config.ts — the map comes from the inventory, reviewed by SEO and business.
import type { NextConfig } from "next";
import map from "./migration/redirects.json"; // [{ source, destination }]

const config: NextConfig = {
  async redirects() {
    // permanent: true answers 308, which search engines treat like a 301.
    return map.map(({ source, destination }) => ({ source, destination, permanent: true }));
  },
  trailingSlash: true, // same as WordPress: avoids two versions of every URL
};
export default config;

Two details matter. The first is trailingSlash: WordPress uses a trailing slash by default, and if the new site does not respect it, every URL has two versions. The second is avoiding chains: if a URL was already redirecting in WordPress, the map must point to the final destination, not the intermediate one.

Middleware comes in when the rule depends on the request: patterns with logic, very large maps that live in a database, or per-language rules. For everything else, the declarative map is easier to review.

Metadata generated on the server

In WordPress, an SEO plugin builds each page's title, description and canonical. In Next.js that job belongs to generateMetadata, and it runs on the server: the metadata is already in the initial HTML.

One Next.js 15 detail: params is now a promise. You have to await it before using it. The example lives in app/blog/[slug]/page.tsx:

import type { Metadata } from "next";
import { getPost } from "@/lib/cms";
type Props = { params: Promise<{ slug: string }> }; // in Next.js 15, params is a promise
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const p = await getPost(slug); // the same request is reused by the page
  return {
    title: p.seoTitle ?? p.title,   // keep the title that already ranks
    description: p.seoDescription,
    alternates: { canonical: `/blog/${slug}/`, languages: { es: `/es/blog/${p.slugEs}/` } },
  };
}

The golden rule is to keep what already works. If a page ranks with a specific title and description, those texts are migrated as they are from the SEO plugin's fields. The migration is not the moment to rewrite titles.

The canonical and hreflang go in the same object. If the site is bilingual, each version declares the other, and both must be reciprocal. Set metadataBase in the root layout so relative paths come out as absolute URLs.

Sitemap and robots as code

The App Router generates sitemap.xml and robots.txt from TypeScript files. That lets the sitemap come from the same source as the pages, so it never goes stale:

// app/sitemap.ts — same source as the pages: it never drifts.
import type { MetadataRoute } from "next";
import { allPosts } from "@/lib/cms";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await allPosts();
  return posts
    .filter((p) => !p.noindex) // what is not indexed is not announced either
    .map((p) => ({
      url: `https://www.example.com/blog/${p.slug}/`,
      lastModified: p.modified, // the real edit date, not the deploy date
    }));
}

A detail often forgotten in test environments: staging must block indexing with robots.ts and authentication, and that rule must not travel to production. More than one migration has shipped with Disallow: / on the public site.

Migrating WordPress to React: what changes in rendering

WordPress delivers complete HTML from PHP on every request. That is the baseline the new site is compared against: everything a crawler used to see must still be in the initial HTML.

In Next.js you achieve that by choosing the right rendering mode for each route:

  • Static. For institutional pages and content that rarely changes. Generated at build time and served from cache. The fastest option.
  • Static with revalidation (ISR). For the blog and catalogs. The page is served from cache and regenerated periodically, or when the CMS signals that something changed.
  • Dynamic on the server. For what depends on the request: search, personalized content.
  • Client only. For interaction that does not need to be indexed: filters, carts, dashboards.

The practical rule: if it must appear in a search engine, it cannot depend on an effect in the browser. I covered that mistake in detail in common useEffect mistakes; here it is enough to say that the main content is resolved in server components.

And WordPress? Many migrations keep it as a headless CMS. Editors keep working the same way and Next.js consumes the content through the REST API or GraphQL. With a webhook on publish, on-demand revalidation updates only the page that changed.

That setup also tackles a silent risk: internal links inside the content. Old posts are full of absolute links to the domain and to WordPress routes. They have to be rewritten during the migration, or every one of them will go through a redirect.

Performance optimization in enterprise environments

Performance is part of SEO through Core Web Vitals. A migration that keeps URLs and metadata but worsens the experience still loses ground.

Next.js provides tools for each metric, but you have to use them deliberately:

  • LCP (main element load). The hero image uses next/image with priority, and the HTML comes from cache. The server should not query the CMS on every visit.
  • CLS (visual stability). Images with declared dimensions and fonts through next/font, which self-hosts them and avoids the jump on load.
  • INP (interaction response). Less JavaScript on the client. Every component marked "use client" adds to the bundle; use it only where there is real interaction.

Third-party scripts deserve their own review. Analytics tags, chat widgets and pixels often weigh more than the site itself. The Script component lets you defer them so they do not compete with the content.

Measure with field data, not just Lighthouse on your laptop. Before migrating, save the real metrics of the WordPress site. They are the baseline the new site is compared against.

Verification before switching DNS

The part most often skipped is the one that protects the most. Before the cutover, the new site is compared to the old one URL by URL, using the raw HTML, without running JavaScript.

This script walks the inventory against staging and reports any difference in status, title or canonical:

# ⚠️ Before the cutover: back up the WordPress database and the DNS configuration.
STAGING="https://staging.example.com"
while read -r url; do
  path="${url#https://www.example.com}"
  # -L follows redirects: what matters is where it ends up and with which code.
  final=$(curl -s -o new.html -L -w '%{http_code} %{num_redirects}' "$STAGING$path")
  title=$(grep -o '<title>[^<]*' new.html | sed 's/<title>//')
  canonical=$(grep -o 'rel="canonical" href="[^"]*' new.html | sed 's/.*href="//')
  echo -e "$path\t$final\t$title\t$canonical"
done < current-urls.txt > comparison.tsv

The result is a table reviewed against clear criteria: no URL returning 404, none with more than one redirect, no empty or duplicated title, and every canonical pointing to the final production URL.

The cutover happens when the table is clean, not when the design is approved. After the DNS switch, the same check runs against production, and Search Console gets watched closely over the following weeks.

Keep the old WordPress privately accessible for a while. If a URL nobody inventoried shows up, you need to be able to check what used to be there.

What NOT to do

  • Redirect everything to the homepage. It is the quick way out when the inventory is incomplete. Search engines treat it as deleted content and each page's ranking is lost.
  • Build the new site as an SPA that paints everything on the client. It looks the same in your browser. In the HTML a crawler receives, the content is missing and every title is identical.
  • Change URLs, design, content and domain in the same cutover. If traffic drops, there is no way to know what caused it. Migrate the platform first; editorial changes come later.

The next step

A WordPress to React migration with Next.js can be neutral for SEO, or even improve it through performance. The method makes the difference: a complete inventory, a reviewed redirect map, server-side metadata, and verification done before the cutover, not after.

If your team is about to migrate a large site to React and cannot afford to lose rankings, or needs to audit a migration that already shipped with problems, at Conqr Digital Agency we design architectures that scale. Let's review your case.

If you want to know how I approach projects like this, take a look at the custom web development service.

FAQ

Does migrating from WordPress to React hurt SEO?
It can hurt a lot if URLs change without redirects, if content is painted only in the browser, or if titles and canonicals are lost. With a complete 301 map, server rendering and equivalent metadata, the migration can be neutral for rankings.
Should I use next.config redirects or middleware?
For a fixed map of old to new URLs, next.config redirects: it is declarative and resolved before rendering. Middleware makes sense when the rule depends on the request, such as patterns with logic or maps that live outside the code.
Can I keep WordPress as the CMS?
Yes. It is a common setup: WordPress stays as the editorial backend and Next.js consumes its content through the REST API or GraphQL. Editors keep their workflow and the frontend gains control over performance and metadata.
Samuel Torres Pichardo

Samuel Torres Pichardo
Software and cloud engineer in Mexico City, with about 3 years of professional experience.
LinkedIn · GitHub · Contact

Professional contact:

Mexico City, Mexico

Available for remote and on-site projects.
B.Eng. in Computing and Telecommunications Technologies

© 2026 Eng. Samuel Torres. Applied Engineering.