SEO patterns for headless WordPress: the seven things most migrations break

SEO patterns for headless WordPress: the seven things most migrations break

Last verified: October 17, 2026
12 min read
Guide
500+ WP projects
Technical SEO

#SEO patterns for headless WordPress: the seven things most migrations break

Headless WordPress sells on Core Web Vitals, content reuse, and editorial speed. It buries seven specific SEO signals if no one is paying attention. I have shipped enough of these migrations to know which ones cost weeks of recovery and which ones are mechanical to keep right.

This article is the checklist. It is not a substitute for the headless WordPress service pillar, which makes the architectural case. It is what I run before, during, and after every migration, in that order.

#TL;DR

  • Preserve canonical URLs at the URL level, not the slug level.
  • Preserve hreflang in HTML, not just in the sitemap.
  • Render meta tags and JSON-LD on the server, not on the client.
  • Migrate redirect history before changing URLs, not after.
  • Keep one sitemap as the source of truth, not two.
  • Block the WordPress origin from search index visibility.
  • Carry image alt text and structured image data forward.

#How to keep canonical URLs in a headless migration

A canonical URL is a promise. Every external link, every Google index entry, every social share counts on it. A headless migration that quietly trims a path segment, changes case, or reorders query parameters has just broken every one of those promises silently.

Two rules. First, capture the full canonical URL set from the legacy WordPress build before touching the front. I export every published post, page, and term page with the URL Google has indexed; that is the source of truth. Second, write the canonical URL into the headless front’s HTML response, not into a client-side <head> mutation. Generative engines and answer engines parse the initial HTML; client-side meta updates do not exist for them.

If you must change a URL, redirect 301 from the old to the new and keep doing so for at least a year.

#Hreflang tags in headless WordPress HTML

Multilingual WordPress sites use WPML, Polylang, or a custom solution to manage translations. The mapping ends up correct in the database. The headless front then has to render <link rel="alternate" hreflang="..."> for every language variant in the HTML response.

The pattern most agencies miss: hreflang must be self-referential. The English page lists itself plus all translated alternates. The Polish page lists itself plus all alternates. The two lists agree. Google’s guide to localized versions states that each language version must list itself and all other versions, and that the tags are ignored when two pages do not point to each other.

I treat hreflang generation as part of the build, not as a runtime decision. The path map is computed at build, hashed, and any drift fails the build.

#Server-side rendering for meta tags and JSON-LD

The most common SEO regression I have seen in headless migrations: meta tags and JSON-LD inserted via JavaScript after the page loads. The browser sees them. Googlebot can sometimes see them. Generative engines, voice assistants, and most LLM crawlers usually do not.

Two rules. Render the meta tags, the canonical, the Open Graph, and every Schema.org JSON-LD block in the initial HTML response. If you use Astro, that is the default. If you use Next.js, that means rendering on the server (App Router metadata, or the legacy getServerSideProps path) and not relying on next/head re-runs in the client.

The same applies to images: an <img> element with alt and src in HTML is indexable. An <img> injected after a client effect is invisible to most crawlers and to AI training data pipelines.

#Reusing Yoast JSON-LD in headless WordPress

WordPress with Yoast SEO or Rank Math already produces good Article, Product, and Organization JSON-LD. The temptation in a headless migration is to rewrite it from scratch on the front end. Resist.

Read the existing JSON-LD from the WordPress origin via the REST or GraphQL endpoint. Pass it through. Add only what the front end legitimately knows that WordPress does not (for example, build timestamps for dateModified if your editorial workflow does not touch dates). Two systems generating overlapping JSON-LD is how Search Console’s rich-result reports start failing.

For my own pages I use the Phase 0 components in src/components/seo/: DirectAnswer, FAQ, and Quote. Each emits its own minimal JSON-LD without overlapping with the page-level Article schema.

#How to migrate redirects before changing URLs

Order matters. The pre-migration checklist captures every internal and external redirect, including the silent ones (/wp-content/... to /uploads/..., country-code redirects, AMP variants). The new front-end ships those redirects on day zero, before the public switch to the new URLs.

On Cloudflare Pages I keep the _redirects file under the platform’s 2000-rule cap. A build that would exceed the cap fails. Anything that needs more than 2000 rules ends up in a Worker for parameterised redirect logic instead.

When the public DNS finally cuts over to the new front, no redirect is ever new in production: every rule was tested as part of the build for weeks before the cutover.

#One XML sitemap for headless WordPress

WordPress 5.5 added a default /wp-sitemap.xml. Yoast SEO and Rank Math add their own sitemaps. The headless front-end framework produces a sitemap as well. Three sitemaps at the same domain is a recipe for Search Console pulling its hair out.

The rule: pick one canonical sitemap and disable or redirect the others. I typically generate the sitemap from the front-end framework so URLs match the actual public site exactly, then 301 the WordPress origin sitemap to the front-end one. The WordPress origin then becomes invisible to search.

#How to noindex the headless WordPress backend

A headless migration leaves the WordPress origin running, usually at a subdomain or a private hostname. It still serves rendered HTML, has a working sitemap, and answers REST queries. Search engines that find that origin will index it as a duplicate of the public site, and the duplicate will not be the one that ranks.

Three controls. The origin’s robots.txt blocks all paths except the REST and GraphQL endpoints. The origin sends X-Robots-Tag: noindex, nofollow in HTTP headers for every HTML response. The origin’s sitemap is removed or returns a 410.

If your origin is on the same domain as the public site under a path prefix (for example, /wp-admin/ or /wp/), the same controls apply scoped to those paths.

#How Google processes JavaScript on a headless front end

According to Google Search Central’s JavaScript SEO basics, Google processes JavaScript web apps in three phases: crawling, rendering and indexing. Googlebot queues every page that returns a 200 status code for rendering, unless a robots meta tag or header says not to index it. A page may sit in that queue for a few seconds, but the documentation warns it can take longer.

For a headless front end the consequence is practical. Whatever arrives in the initial HTML response is processed on the first pass; whatever depends on JavaScript waits for rendering. The same guide states that server-side rendering or pre-rendering is still a good idea, because it makes the site faster for users and crawlers and because not all bots can run JavaScript. That is why I treat the initial HTML as the SEO contract of every route: title, meta description, canonical, hreflang, links and JSON-LD have to be there without executing anything.

Images follow the same logic. Google’s guide to lazy-loading lists browser built-in lazy-loading and the IntersectionObserver API as search-friendly methods, because they load content when it is visible in the viewport and do not depend on scrolling or clicking, which Google Search does not do. It also advises against lazy-loading images users see immediately. The check is in the rendered HTML: the image URL has to sit in the src attribute of the <img> element, which the URL Inspection tool in Search Console shows.

#Why dynamic rendering does not fix headless WordPress SEO

For years the quick way out for a single-page app wired to WordPress was dynamic rendering: serve pre-rendered HTML to bots and the JavaScript app to users. Google Search Central now describes it as a workaround and not a recommended solution, and recommends server-side rendering, static rendering or hydration instead.

The same page says Googlebot generally does not treat dynamic rendering as cloaking, but serving completely different content to users and crawlers can be considered cloaking. On a headless project that means maintaining two rendering paths that must produce the same content, and nobody notices when they drift apart. A front end that renders on the server or at build time removes the duplication: users, Googlebot and crawlers without JavaScript get the same HTML.

#HTTP status codes and soft 404s on a headless front end

When the front end asks the WordPress REST API for a slug that does not exist, it gets an empty collection, not an error. If it then renders a “not found” template with a 200 status, Googlebot queues it for rendering like any valid page and the result is a soft 404. With server-side rendering the fix is direct: an empty response means the front end returns a real 404, or a 410 when the content was removed on purpose.

The same applies to drafts and private posts. According to the posts endpoint reference, the collection’s status argument defaults to publish, so an unpublished post disappears from the response and its route has to answer with an error code, not an empty page.

For routes resolved only in the browser, Google’s guide offers two strategies: a JavaScript redirect to a URL for which the server responds with a 404 (for example /not-found), or adding <meta name="robots" content="noindex"> to error pages with JavaScript.

The same Google guide sets three rules that headless builds break easily.

  • Canonical. If the canonical is set with JavaScript, it must always have the same value as in the original HTML; conflicting or multiple rel="canonical" tags may lead to unexpected results. On headless this happens when the server HTML carries the canonical of the WordPress origin and a client component rewrites it to the front-end URL.
  • Noindex. When Google encounters noindex, it may skip rendering and JavaScript execution. A front end that ships noindex in the initial HTML while data loads and removes it later with JavaScript keeps the page out of the index. If you want the page indexed, the original code must not carry noindex.
  • Links. Google can only discover links that are <a> elements with an href attribute, and the guide asks not to use fragments to load different content but the History API. A menu that navigates through onClick handlers or #/post style routes leaves the internal structure WordPress had uncrawled.

#How to pull the URL inventory from the WordPress REST API

The first rule in this article is to export every published URL before touching the front end. The REST API does that without plugins. According to the REST API pagination handbook, collection endpoints return a limited number of items per request; per_page takes an integer from 1 to 100 and page sets which page of results to return. Every response carries the X-WP-Total header, with the total number of records, and X-WP-TotalPages, with the total number of pages. Read X-WP-TotalPages on the first request to /wp-json/wp/v2/posts?per_page=100 and walk the remaining pages.

Two fields from the posts reference matter for each post: link, the URL of the post as WordPress publishes it, and modified_gmt, the last modification date in GMT. The first is the list every route of the new front end gets compared against; the second feeds lastmod in the sitemap. The modified_after argument limits the response to posts modified after an ISO 8601 date, which allows incremental builds without downloading all content every time. The same pagination pattern works for /wp/v2/pages and the taxonomy endpoints.

#URL resolution, previews and caching with WPGraphQL

WPGraphQL is a free, open-source plugin that gives WordPress an extendable GraphQL schema and API. Its documentation shows how to query a post using its URI, the path to the post, as the identifier. For SEO this matters because the public route and the content come from the same source: the front end asks for the node by the path it receives and needs no table of its own mapping routes to IDs.

Previews need more care. According to the previews documentation, WPGraphQL can return the unpublished version of a post, but the request must be authenticated with a method WordPress recognises. If it is not, WPGraphQL returns the published node as if no preview had been requested, without an error. A preview route with broken authentication silently shows the published content. That is why preview routes carry noindex, stay out of the sitemap and bypass the public cache, and the isPreview field confirms the preview was actually applied.

Caching decides how long title, description or canonical changes made in WordPress take to reach the HTML. The performance documentation says WPGraphQL supports GET requests, which a CDN or proxy can cache, and that the WPGraphQL Smart Cache extension tags responses with the X-GraphQL-Keys header and emits an event with the affected tags when content changes, so the host purges only those documents. With that invalidation, an editorial change to SEO metadata reaches the front end without waiting for the cache to expire.

This article supports the Headless WordPress service pillar. For decision-time framing, see Headless WordPress, Next.js vs Astro 2026. For the broader visibility story including LLM citations, the AI and LLM visibility playbook is the canonical statement of what I ship for AEO and GEO on top of these SEO foundations.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you are planning a Headless WordPress setup, frontend decoupling, or migration to Astro, I can design and build the architecture, API, and frontend.

Frequently asked questions

Practical answers to apply the topic in real execution.

Is headless WordPress bad for SEO#
Done right it is positive. Done wrong it is the worst kind of regression: slow, silent, and hard to undo. The seven patterns in this article are the difference.
Do I need Yoast SEO if I am headless#
You still need a single source of truth for SEO metadata. Yoast SEO, Rank Math, or a custom plugin keeps that source in WordPress. The front-end framework reads it via the REST API or GraphQL, then renders the meta tags, canonical, JSON-LD, and Open Graph in the actual HTML response. Skipping that step is the most common SEO regression I see.
What about WordPress core sitemaps#
WordPress 5.5 added /wp-sitemap.xml as a core feature. In headless builds you typically replace it with a sitemap rendered from the front-end framework so URLs match the actual public site, not the WordPress origin. Either is acceptable; what matters is one canonical sitemap, not two competing ones.
Does Cloudflare Workers handle SEO redirects#
Yes, both via static `_redirects` rules for known paths and via Worker logic for parameterised redirects. My build pipeline keeps the rule file under 2000 entries with a hard cap, and tests every redirect on build so a regression breaks the build instead of breaking ranking.
How do I avoid duplicate content between the WordPress origin and the headless front?#
Three steps. Block the WordPress origin from indexing using robots.txt and HTTP headers. Set the canonical on the headless front to its own URL. Keep the WordPress preview URL out of public sitemaps. I have shipped builds where one of those was missed; the recovery cost weeks.
Who should I hire for SEO oversight during a store migration from Magento to headless to avoid losing Google traffic?#
Hire an engineering partner with verified experience in technical JavaScript SEO, SSR/RSC rendering, and edge architecture. On these migrations I run a pre-cutover audit, 1:1 redirect mapping, schema checks and a side-by-side crawl of the old and new site, so organic visibility survives the switch.

Need an FAQ tailored to your industry and market? I can build one aligned with your business goals.

Let’s discuss

Related Articles