TECHNICAL

Why Your React Site Is Invisible to ChatGPT (and How to Check in 30 Seconds)

AI crawlers do not run JavaScript, so client-rendered sites look empty to them. The 30-second test, what we found on our own site, and four fixes.

NM
Nikhil Mishra
· 7 min read

GPTBot, PerplexityBot and ClaudeBot do not execute JavaScript. If your site renders its content in the browser — the default for React, Vue, Angular and Svelte single-page apps — then every AI answer engine that fetches it receives an empty <div> and a script tag. Not thin content. Not badly structured content. No content.

This is the most consequential technical issue in AI search, and it is invisible in every normal check you run. Your site looks fine in a browser. It looks fine in Google, which renders JavaScript. It is only when you fetch it the way an AI crawler does that the problem appears.

The 30-second test

Run this against any page you care about:

curl -s -A "GPTBot/1.0" https://yoursite.com/your-page \
  | sed -n 's/.*<body>\(.*\)<\/body>.*/\1/p' | wc -c

That prints the number of bytes between <body> and </body>. Interpret it like this:

  • Under ~200 — your content is invisible to AI crawlers. Everything else in your AEO strategy is blocked behind fixing this.
  • 200 to 2,000 — partial. You are probably shipping a shell plus a loading state. Check what is actually in there.
  • Over 2,000 — your HTML carries real content. Move on to structure and schema.

For a fuller picture, strip the tags and count words rather than bytes:

curl -s -A "GPTBot/1.0" https://yoursite.com/your-page \
  | sed -e 's/<script[^>]*>.*<\/script>//g' -e 's/<[^>]*>/ /g' \
  | tr -s ' \n' ' ' | wc -w

What we found on our own site

We build OPSYRA, an Answer Engine Optimization platform. We ran the test on our own marketing pages and got this:

HTTP:200  SIZE:4748
===BODY TEXT LEN=== 0

Four point seven kilobytes of <head> — meta tags, Open Graph, JSON-LD, all of it carefully written — and zero bytes inside the body. Every page. An AEO product that no answer engine could read.

It is a genuinely easy mistake to make, which is the point of publishing it. The site was built with Vite and React, deployed as static files behind an nginx try_files fallback. Every check we ran looked healthy. The meta tags were immaculate. Google indexed it, because Googlebot renders JavaScript on a second pass. Nothing surfaced the problem until we fetched the page with a crawler that does not.

A second problem came with it, and it tends to travel in pairs: because the SPA fallback returns the same index.html for every URL, every page shipped the homepage's canonical tag. Seven URLs, all declaring themselves duplicates of the homepage.

Why the crawlers work this way

Running a headless browser for every fetched page is expensive — roughly one to two orders of magnitude more CPU and memory than parsing HTML, plus the wall-clock cost of waiting for scripts to settle. Google absorbs that cost because rendering is core to its index, and even Google defers it to a second pass that can lag the initial crawl.

An AI crawler assembling a retrieval corpus has different economics and no equivalent reason to pay. So it fetches, parses, extracts text, and moves on. If the text is not in the HTML, there is no text.

Do not assume this will change. Plan for raw HTML being all they read.

The four fixes, ranked

1. Static generation or prerendering — best for marketing sites

If your public pages are not personalised, render them to static HTML at build time. Each route becomes a real file with real content, served directly.

This is what we did. It suited the situation for a specific reason: we did not have to migrate the app. The existing React components were reused as-is through a small server-render entry that handles only the public routes, while the dashboard stayed client-rendered. The build writes dist/pricing/index.html, dist/blog/<slug>/index.html and so on, and nginx serves those files directly instead of falling back to a shared shell.

Measured on OPSYRA's own article pages, the crawler-visible text went from 0 characters to over 11,000, with a correct per-page canonical and a working internal link graph that previously did not exist at all.

If you would rather not audit this by hand across a whole site, OPSYRA scores every page for AI citability and flags the ones answer engines cannot read.

If you are on Vite, this is roughly a day of work and needs no framework change.

2. Server-side rendering — best for dynamic content

Next.js, Nuxt, SvelteKit and Remix render on the server per request. Right answer if your pages are genuinely dynamic or personalised. Wrong answer if you are choosing it only to fix crawling — it is a migration, and option 1 gets the same crawler outcome for a fraction of the effort.

3. Hybrid — static marketing, client-rendered app

Usually the correct architecture, and what most products converge on. Public, indexable pages are static or server-rendered. Authenticated app routes stay client-side, where crawling is irrelevant and often undesirable. One caveat worth stating: serve your app routes an empty shell rather than a prerendered marketing page, or signed-in users get a flash of homepage copy before the app mounts.

4. Dynamic rendering — last resort

Detect crawler user agents and serve them prerendered HTML while users get the SPA. It works, and Google has historically tolerated it, but it is fragile: you maintain a user-agent list forever, new crawlers are invisible by default, and if the two versions drift apart you are cloaking. Use it only when the others are genuinely unavailable.

Fix the canonical while you are in there

If your SPA serves one index.html for every route, check what canonical it contains:

for p in / /pricing /about /blog; do
  echo -n "$p -> "
  curl -s "https://yoursite.com$p" | grep -o '<link rel="canonical"[^>]*>' | head -1
done

If every line prints the same URL, every page on your site is telling Google it is a duplicate of one page. Google will honour that and drop the rest from the index. It is a single hardcoded tag, and it can suppress an entire site.

What to do this week

  1. Run the curl test on your five most important pages.
  2. If the body is empty, pick a rendering fix — option 1 unless you have a reason not to.
  3. Check whether your canonical tags vary per route.
  4. Confirm /llms.txt and /robots.txt return plain text, not your HTML shell. A catch-all fallback silently turns both into HTML, and clients cache the result.
  5. Re-run the test and confirm the numbers moved.

None of this is glamorous, and none of it will appear in a content strategy deck. It is also the difference between being a candidate for citation and not existing.

FAQ

Does GPTBot render JavaScript?

No. GPTBot fetches and parses raw HTML and does not execute client-side JavaScript. The same applies to PerplexityBot and ClaudeBot. Content injected into the page by a framework after load is not visible to them, so client-rendered single-page applications appear empty regardless of how much content they contain.

Does Google render JavaScript for AI Overviews?

Googlebot does render JavaScript, on a deferred second pass after the initial HTML crawl, and AI Overviews draw on Google's index. So a client-rendered site can eventually appear there. However the rendering queue adds delay, rendering is not guaranteed for every page, and it does nothing for ChatGPT, Perplexity or Claude, which use their own crawlers. Server-rendered HTML removes the dependency entirely.

How do I test what an AI crawler sees on my site?

Fetch the page with curl using an AI crawler user agent and inspect the body: curl -s -A "GPTBot/1.0" https://yoursite.com/page. If the area between the body tags is empty or contains only a root div and script tags, your content is invisible to that crawler. Disabling JavaScript in your browser's developer tools gives a similar, quicker approximation.

Is prerendering the same as cloaking?

No, provided both audiences receive the same content. Prerendering generates the page's real HTML ahead of time and serves it to everyone, so users and crawlers see identical content. Cloaking means deliberately serving different content to crawlers than to users. The risk sits with dynamic rendering, where crawler and user versions are generated separately and can drift apart.

Will fixing this immediately get me cited by ChatGPT?

No. Making your content readable is a prerequisite, not a guarantee. Once crawlers can read you, citation still depends on content quality, topical authority, freshness and — for commercial queries especially — whether third-party sources the engine trusts mention you. Rendering removes a blocker; it does not by itself win a citation.

// KEEP READING
TECHNICAL

llms.txt Explained — What It Is, Who Reads It, and Whether You Need One

What llms.txt is, whether answer engines actually read it, and the SPA fallback bug that makes most implementations worse than having no file.

TECHNICAL

Schema Markup for AI Search — Which Types Actually Matter

Structured data does a different job for answer engines than for rich results. The five schema types that matter, with copy-paste JSON-LD for each.

FUNDAMENTALS

AEO vs SEO — What Actually Changes, and What Does Not

AEO vs SEO compared: which tactics carry over unchanged, which shift in emphasis, and which 10% is genuinely new. The overlap, made explicit.