TL;DR
Most of an audit on a headless build is the same audit. The questions do not change. What changes is that half your usual checks return the wrong answer if you run them the usual way. Crawl twice, raw and rendered, because the difference between the two is the finding.
Most of an audit on a headless or React site is the same audit. The questions do not change. Is it crawlable, does it say what it is, is anything removing pages from search. What changes is that half your usual checks return the wrong answer if you run them the way you normally would.
So this is the list of things I do differently, rather than another explanation of what headless means.
Your tools will lie to you by default
Turn on JavaScript rendering in the crawler before you start. Screaming Frog and Sitebulb both crawl raw HTML unless told otherwise. On a client-rendered site that produces a crawl of empty shells with no headings, no content and almost no internal links, and a report that looks catastrophic and is meaningless.
The reverse mistake matters too: once rendering is on, the crawl shows you the rendered page and hides the very gap you are trying to find. Run both. The difference between the two crawls is the finding, and it is the single most useful artefact you can produce on one of these sites.
Expect it to be slow. A rendered crawl is an order of magnitude slower. Budget for it or sample.
There is a reason for that, and it is the same reason Google rations rendering. HTTP Archive’s 2025 Web Almanac puts the median mobile home page at 632 KB of JavaScript against 22 KB of HTML. Rendering means fetching and executing all of it, per URL, and neither your crawler nor Googlebot does that for free.
The head is assembled at runtime
On a traditional site the <head> is written by a template and you can read it. On a headless build it is often composed by a component, which means it can differ by route, by data state, and occasionally by whether an API call succeeded.
What I check: the canonical, the title, meta robots and hreflang, in the raw response, on at least one page of each route type. Not one page. One of each type, because the composition logic is per-route and the bugs are per-route.
The 2025 Web Almanac found an invalid element inside the <head> on 10.3% of mobile pages, most often a stray <img> or <div>. That matters more here than anywhere, because the head closes at the first invalid element, and a head assembled from components is exactly where a stray wrapper div gets introduced by someone who has never thought about it.
Routing questions that do not exist elsewhere
- Does every view have a URL? The classic headless failure is content reachable only through interaction, a tab, a modal, a state change, with no address of its own. If it has no URL it does not exist as far as search is concerned, and that is an architecture conversation, not a fix.
- What does the server return for a URL that does not exist? Many SPA setups return 200 with the app shell for everything, which means every typo is a soft 404 and there is an infinite supply of them.
- Are the links real anchors? A
<div onClick>that routes is not a link. It works for people and it is invisible as a link to a crawler. This is common and it silently removes your internal linking. - Is anything a heading that should not be? Component libraries hand out
<h2>and<h3>as typography rather than structure, so a card component used forty times produces forty headings. Same failure as a theme wrapping the menu in H2 tags, arrived at through a different route. - Is pagination real? Infinite scroll with no paginated URLs means everything past the first screen is unreachable.
Who you are actually briefing
The thing that most changes the engagement is not technical. On a WordPress site the person implementing is often the person you are talking to. On a headless build it is a development team with a sprint cadence, a backlog and their own priorities.
That changes what a good recommendation looks like. “Add a canonical” is not a task. “The canonical is composed in the SEO component and returns undefined on the product route when the API response lacks a slug” is a ticket someone can pick up. Three well-specified tickets beat sixty findings, and on these projects that is not a preference, it is the difference between shipping and not.
What is usually fine
Worth saying, because these audits attract alarmism. Google renders JavaScript and has for years. A well-built Next or Nuxt site with server-side rendering has no inherent disadvantage, and some of them are the fastest sites I look at.
The problems are not caused by the framework. They are caused by rendering decisions made by people who were not thinking about crawlers, which is a reasonable thing for a front-end developer not to be thinking about.
The short version
- Crawl twice, raw and rendered. The difference is the finding.
- A raw-only crawl of a client-rendered site is meaningless.
- Check the head per route type, not per page. Bugs are per-route.
- One page in ten has an invalid element in the head, and everything after it is ignored.
- Does every view have a URL? Are the links real anchors?
- Check what a nonexistent URL returns. 200 for everything means infinite soft 404s.
- Write tickets, not findings. You are briefing a sprint, not a site owner.
If a site should be ranking and it isn’t, that’s the work I do. Technical SEO covers rendering, routing and the crawl layer. If you’re not sure which of several plausible problems is costing you, that’s what an SEO audit is for.

