Skip to content
What To Fix First

Your content only exists after JavaScript runs

Googlebot renders JavaScript; the fetchers behind ChatGPT, Claude and Perplexity mostly do not. How to check what they see, and how to server-render or pre-render on each framework.

Two views of the same page: the HTML the server sends contains only an empty root div and a script tag, which is all a non-rendering AI crawler sees; the page a browser shows after the script runs is full of content.what the server sends<div id="root"></div><script src=app.js>0 wordsscript runswhat a browser shows1,200 wordsAI crawlers stop hereonly Googlebot gets this far
This diagram is free to reuse with credit — all eighteen, as SVG or PNG.

What is actually happening

A single-page application sends the browser a tiny HTML file — a <div id="root"> and a script tag — and the script then builds the page. A person with a browser never notices. A crawler that does not execute the script gets the tiny file and stops: as far as it can tell, the page has a title and nothing else. The AI visibility check reports this as “the page is empty until JavaScript runs” and counts the words it found — if that number is under about forty, this is your problem.

Step 1 — confirm it from the source, not the inspector

# What a non-rendering crawler receives:
curl -s -A "Mozilla/5.0 (compatible; ClaudeBot/1.0)" https://example.com/ \
  | sed 's/<[^>]*>/ /g' | tr -s ' \n' | head -c 600

If that prints your headline and first paragraph, you are fine and something else is going on. If it prints menu labels and “You need to enable JavaScript to run this app”, read on.

Step 2 — pick the fix that matches how the site is built

Already on a framework that can server-render

Next.js, Nuxt, SvelteKit, Remix and Astro all render on the server by default. Sites built on them that still ship empty HTML have usually opted out per page — a component marked client-only that wraps the whole page, data fetched in a useEffect instead of on the server, or a build configured for a static export with the content loaded afterwards. Find the page component and move the data fetch to the server side (Next.js: a Server Component or getServerSideProps; Nuxt: useAsyncData; SvelteKit: +page.server.ts). The markup then arrives with the content already in it.

A bare React / Vue / Angular app

Two honest options. The thorough one is migrating the app to a server-rendering framework — Next.js for React, Nuxt for Vue, Angular Universal for Angular. It is real work, and it is the fix that also improves speed and Google indexing. The quick one is pre-rendering: at build time, run the app in a headless browser for every route and save the resulting HTML as static files. Vite’s vite-plugin-ssr/vike, react-snap and Angular’s prerender builder do this. It works for sites whose content is the same for every visitor — marketing pages, docs, blogs.

Content that changes per request, on an app you cannot rebuild

A dynamic rendering service sits in front of your site, detects crawler user-agents, renders the page in a headless browser and serves them the finished HTML while humans get the app as before. Prerender.io is the established one; Cloudflare Workers can do it yourself with Browser Rendering. It is a workaround rather than a fix — you are now maintaining two versions of every page — but it can be turned on in an afternoon.

Step 3 — do not lose the labels in the process

Whichever route you take, make sure the server-rendered HTML also includes the <title>, meta description, canonical and any JSON-LD. Client-side “helmet” libraries that set the title after load have the same problem as the content did. The structured data guide covers what to put there.

Check it worked

Re-run the check: “Content without JavaScript” should read Present with a word count that matches what is on screen, and the headline should move on to whatever is next — or to all clear.

Common questions

Google indexes my site fine. Why would AI crawlers be different?
Googlebot runs a full headless Chrome and executes your JavaScript, at some delay. The fetchers behind ChatGPT, Claude and Perplexity are much simpler: they request the URL and read whatever HTML comes back. OpenAI has said GPTBot does not execute JavaScript; Anthropic's and Perplexity's crawlers behave the same way in practice. If your content is not in that first response, they see a header, a footer and an empty div.
How do I see what a non-rendering crawler sees?
View the page source (Ctrl+U), not the DevTools inspector — the inspector shows the page after JavaScript ran. Search the source for a sentence from your main content. If it is not there, it is not there for AI crawlers either. The AI visibility check does this automatically and counts the visible words.
Is this the same problem as 'unused JavaScript'?
No. Unused JavaScript is about speed — shipping code that never runs. This is about where the content lives. A page can be fast and still empty without JavaScript, and slow while fully server-rendered.
My site is Shopify / Wix / Squarespace / WordPress. Do I have this problem?
Almost certainly not. Those platforms render pages on the server; the HTML they send already contains the content. The problem is specific to sites built as a JavaScript application — React, Vue or Angular with a bare index.html — and deployed without server-side rendering or pre-rendering.
What about a headless CMS with a React front end?
That is the most common way to end up here. The CMS is fine; the front end is what decides. Move it to a framework that server-renders (Next.js, Nuxt, SvelteKit, Astro) or put a pre-rendering layer in front of it. The content does not have to move.

Check a page against this

Free, no signup. Reads robots.txt and the page as a browser and as three AI crawlers. Takes about ten seconds; nothing is stored.

Enter any public URL. You’ll get the one thing stopping AI crawlers from reading the page (if anything is), then a crawler-by-crawler table and what the HTML actually tells a machine.

Other guides