Your Single-Page App Serves an Empty Page to an AI Crawler
Open your client-rendered site in a browser and it looks fine: the hero renders, the copy is there, the nav works. Now fetch the same URL the way an automated client does — no browser, no JavaScript engine, just an HTTP GET that reads the response body — and you may get a near-empty document. A <div id="root"></div>, a couple of <script src=…> tags, and almost no human-readable text. The browser turned that shell into a page by downloading and running the bundle. A client that does not run the bundle never gets past the shell.
That gap matters more every quarter, because a growing share of the clients hitting your URLs are not browsers. They are the crawlers behind AI answer engines — GPTBot, ClaudeBot, PerplexityBot, Google-Extended, and the fetchers that back "browse" and live-citation features. Many of them read your initial HTML and stop there. If your content only exists after a client bundle runs, then as far as those agents are concerned your content does not exist.
The three states of your initial HTML
The only HTML that is guaranteed to be seen is the bytes that come back from a plain GET, before a single line of your JavaScript executes. Every page falls into one of three buckets when you look at just those bytes:
- Server-rendered. The response already carries the page's real content: headings, paragraphs, links, and ideally a
<script type="application/ld+json">block. A non-executing agent reads it and understands the page. This is the goal. - Thin. Not much text, but no obvious single-page-app scaffolding either — a landing page that is genuinely sparse, or a page whose content is light. Ambiguous, and usually not an emergency.
- JS-only shell. Little to no visible text, a recognizable SPA mount node (
<div id="root">,<div id="__next">,data-reactroot), and an external script bundle. The content is real, but it lives inside the bundle and only appears after the bundle runs. To a non-rendering agent this is an empty page.
The shell is the failure mode, and it is a high-confidence one: little visible text and a known mount node and a JS bundle, all three together, is not an ambiguous signal. It is a page that has outsourced its entire content layer to the client.
How to see what the crawler sees
You cannot judge this from your browser's DevTools inspector — that shows the computed DOM, after your JavaScript has already run and populated everything. You have to look at the raw response.
curl -s https://your-site.com/ | head -c 2000
If what comes back is your article text, your product copy, your JSON-LD — good. If what comes back is <div id="root"></div> wrapped in boilerplate and script tags, that is exactly what a non-rendering agent receives. "View Source" in the browser (as opposed to "Inspect") shows the same thing: the document as delivered, not as rendered.
A quick rule of thumb that mirrors how an automated classifier draws the line: under roughly 500 characters of visible text in the raw HTML, plus a SPA mount node, plus a script bundle, and you are looking at a shell.
"But Googlebot renders JavaScript"
It does — eventually, and that is the catch. Googlebot's rendering is a second wave: it indexes the raw HTML first and comes back to execute JavaScript later, when render budget is available, which can lag by anywhere from minutes to days. For a marketing page that changes rarely, you may get away with it. For anything time-sensitive, the render arrives after it mattered.
More to the point, Googlebot is not the crawler you are worried about here. The crawlers feeding AI answer engines are far less consistent about executing JavaScript, and several do not execute it at all. Betting your visibility in ChatGPT, Claude, and Perplexity on "the crawler will run my bundle" is betting on a behavior most of them do not have. The safe assumption is the pessimistic one: assume the agent reads your initial HTML and nothing else.
There is a second-order cost, too. Structured data — the schema.org JSON-LD that answer engines lean on to understand what a page is — is frequently injected by the same client bundle. If your content is a shell, your structured data is usually a shell as well, so you lose the machine-readable layer at the same time you lose the text.
The fix is rendering strategy, not a crawler trick
There is no meta tag that makes a shell legible. The content has to be in the initial HTML. The paths there, roughly in order of how well they hold up:
- Static generation (SSG) / static export for content that does not change per request — blog posts, docs, marketing, pricing. The HTML is built ahead of time and served whole. It is the most robust option and, conveniently, the cheapest to host. (This site is a static export for exactly this reason.)
- Server-side rendering (SSR) for content that is dynamic but still needs to be present on first byte. The server renders the HTML, the client hydrates on top. Frameworks like Next.js, Nuxt, SvelteKit, and Remix make this the default rather than the exception.
- Prerendering / dynamic rendering as a bridge if you are stuck on a pure CSR stack you cannot rearchitect yet: a prerender service or your CDN serves a rendered snapshot to bots. It is a patch, not a destination — the snapshot can drift from the live app — but it beats shipping a shell.
Whichever you pick, the test is the same one you started with: curl the URL, and make sure your real content and your JSON-LD are in the bytes that come back.
Check it in one fetch
Merlonix's free Agent-Readiness checker does exactly the non-rendering fetch described above: it requests your homepage the way an agent that does not execute JavaScript would, measures the visible text, looks for a SPA mount node and a script bundle, and labels the page server-rendered, thin, or JS-only shell — capping the overall grade at C for a shell, because content a crawler cannot read is content that cannot be cited. In the same pass it reads your /robots.txt AI-bot rules and checks your homepage for schema.org JSON-LD, so you see the whole "can an agent fetch and understand this page" picture at once. It is public data only — no account, no verification.
The paid monitoring tiers keep watching it, so a deploy that quietly flips a server-rendered page into a JS-only shell — a routing change, a framework upgrade, a getServerSideProps that became a client fetch — is something you find out about, instead of something an answer engine discovers for you.
→ Related: robots.txt for AI Crawlers: Why Blocking GPTBot Doesn't Remove You From ChatGPT → Related: Structured Data for AI Answer Engines: Why JSON-LD Decides Whether You Get Cited → Related: Hidden Text an AI Agent Reads but a Human Can't