Contents
Headless WordPress is fast, flexible and pleasant to build with. It can also quietly wreck your SEO. My own site, danishishfaq.online, runs on headless WordPress: posts are written in WordPress and shown by a React front end. When I audited it, Google had shown the site in search just 6 times in 90 days, with zero clicks.
The content wasn’t the problem. Four technical issues were, and all four are common on headless builds. This is what I found, how I fixed each one, and a checklist you can run on your own headless site.
The setup: how this site works#
The architecture is simple, and very typical:
- WordPress lives on a subdomain,
cms.danishishfaq.online. That’s where I write posts, with Yoast SEO installed. - The public site on
danishishfaq.onlineis static HTML with React “islands” for interactive parts: the blog list, single posts, comments and the contact form. - React fetches posts from the WordPress REST API (
/wp-json/wp/v2/posts) in the visitor’s browser.
It looked great to a visitor. To a search engine, it looked very different.
Problem 1: the CMS subdomain was competing with the real site#
This was the biggest issue. The WordPress install on the subdomain was still a fully public, indexable website. It had its own theme, its own copies of every post, a Yoast XML sitemap, and a robots.txt that pointed Google straight at that sitemap.
Worse, every post’s canonical tag pointed to the subdomain. So Google saw two copies of each article and was told the CMS copy was the original. The main site, the one I actually wanted to rank, was being labelled the duplicate.
The fix#
I made the subdomain a private back end instead of a second website:
- 301 redirects for every public WordPress URL. A small PHP snippet (I use WPCode) hooks into
template_redirect. Single posts go todanishishfaq.online/blog/{slug}, the CMS home goes to the main homepage, and everything else (categories, tags, archives, 404s) goes to/blogs. Logged-in editors, previews, the REST API,wp-admin,wp-login.php, images inwp-contentandrobots.txtare left alone. - Canonicals rewritten to the main domain. I filtered
post_linkso WordPress itself believes each post lives at/blog/{slug}on the main site. Yoast caches permalinks in its “indexables” table, so I also filteredwpseo_canonicalandwpseo_opengraph_url. - CMS sitemaps switched off. I disabled WordPress core sitemaps with the
wp_sitemaps_enabledfilter and turned off Yoast’s XML sitemaps in its settings. - A noindex header on API responses so raw JSON never shows up in search.
Why redirects and not just noindex? A noindex tag only removes the duplicate. A 301 also passes any links and signals the CMS URLs had picked up across to the real pages.
Problem 2: blog posts were empty until JavaScript ran#
When I requested a blog URL the way a crawler does, without running JavaScript, every post returned the same page. It had about 39 words, the title “Post — Danish Ishfaq”, a generic meta description, no H1 and no canonical tag. The article only appeared after React fetched it from the API.
Google can render JavaScript, but rendering is queued separately from crawling and can be delayed. Most other crawlers don’t render at all. That includes social networks building link previews and many AI search crawlers. To all of them, every post on my blog looked identical and empty.
The fix: server-render the post, keep React for the interactive parts#
I didn’t need Next.js or a Node server. The site is on ordinary shared hosting, which runs PHP, so I added a small PHP renderer:
- An
.htaccessrule sends/blog/{slug}toblog-post.php. - The script fetches the post from the WordPress REST API on the server and caches the response for 10 minutes. It keeps a stale copy in case WordPress is briefly unreachable.
- It fills the existing HTML template with the real title, meta description, canonical, Open Graph and Twitter tags,
BlogPostingandBreadcrumbListstructured data, and the full article HTML. - React now skips re-rendering anything marked as server-rendered and only mounts the comments section on top.
Without JavaScript, each post now comes back as a complete page of about 1,000 words. The homepage and /blogs list get the same treatment, so crawlers can follow real links to every post.
Problem 3: made-up URLs returned “200 OK” (soft 404s)#
Because every /blog/anything URL served the same shell page, a mistyped or invented address also returned 200 OK instead of 404. Google calls these soft 404s. They waste crawl budget and can make the whole section look low quality.
The fix#
The renderer knows when the API returns no post for a slug, so it now sends a real 404 with noindex and a short list of the latest posts for the visitor. If WordPress itself is down and there’s no cached copy, it returns 503 with a Retry-After header. That tells Google to come back later instead of dropping the page.
Problem 4: the sitemap didn’t list a single post#
The main site’s sitemap.xml was a static file listing seven pages. None of the blog posts were in it. (They were in the CMS sitemap, which is the one we just switched off.)
The fix#
/sitemap.xml is now generated by PHP. It lists the static pages plus every published post, with a lastmod date taken from WordPress. A new post appears in the sitemap within minutes of publishing, with no manual step. I submitted it in Google Search Console, which read it straight away.
Headless WordPress SEO checklist#
Run these checks on your own headless site. The first two catch most of the damage.
- Is the CMS publicly indexable? Open a post on your CMS domain in a private window. If it loads as a page instead of redirecting to your front end, it’s competing with your site.
- Where do your canonicals point? Check the canonical tag on your front-end pages and in Yoast’s
yoast_head_jsonin the REST API. Both should use your public domain. - What do crawlers get without JavaScript? Run
curl https://yoursite.com/blog/a-post, or view the page source rather than the inspector. You should see the title, meta description and article text in the raw HTML. - Do fake URLs return 404? Request
/blog/this-does-not-existand check the status code, not just what the page says. - Does your sitemap include posts, and update itself?
- Are Open Graph tags per post? Paste a post URL into a LinkedIn or WhatsApp message and see whether the preview shows that post’s title and image.
- Is there structured data? Test a post with Google’s Rich Results Test and look for
BlogPostingorArticle.
What’s next#
These fixes went live in October 2026. Google needs a few weeks to recrawl, drop the CMS duplicates and index the real pages. I’ll update this post with the Search Console numbers once there’s enough data to show.
If you run a headless WordPress site, or any JavaScript front end, and traffic isn’t matching the effort you put into content, these four issues are the first place I’d look. You can run a free audit of your site (here’s how to read the audit report), or get in touch and I’ll take a look. You can also read why I’d choose a custom front end versus a CMS in the first place.
I build fast Shopify stores and WordPress sites with technical SEO engineered in, and AI automation that takes repetitive work off your team.
Need help with your Shopify store, WordPress site or AI automation?
Tell me what you’re working on, or start with a free audit of your site.



