JavaScript and AI Crawlers
Most AI crawlers fetch your HTML and do not execute JavaScript, so any content injected by the browser after load is not in what they receive. If your services, pricing, contact details or body copy only exist after hydration, the crawler gets an empty shell where the answer should be.
What we measured
25.0% of sites hid at least one answer-critical element from a non-rendering AI crawler. Body text was the most commonly hidden element at 15.5%, followed by name, address and phone at 14.7%, internal links at 13.9%, and the contact or booking form at 11.1%. The predictor was not whether a site was custom built. It was whether the platform server renders answer-critical content by default: 4.0% of managed server-rendered builds were affected against 28.3% of everything else, a two-proportion z of 3.69 at p below .001.
Source: AI Visibility Study 2026, dual capture with a GPTBot user agent and no JavaScript against a headless Chrome rendered DOM, diffed per element. Data locked 2026-06-07., n=368.
Sample framing: 405 sites attempted, 368 analyzed, a 90.9% response. Purposive North American B2B prospect list, skewing construction, trades and SMB services. It represents the businesses small agencies actually pitch, not a random sample of the web.
How to re-derive it: Open dataset at /research/ai-visibility-2026/dataset.csv, 368 anonymized rows, reproduces every figure. SSRN preprint 6915818.
Why it matters
- The measure is first-party crawl visibility to non-rendering crawlers such as GPTBot, ClaudeBot and PerplexityBot. It is not a claim that an assistant cannot know you exist. Assistants also read search indexes and retrieval layers, and those may have rendered your page even when a direct fetch did not.
- What it does mean is that the copy you control, on the domain you own, is not what those crawlers received. Every other source describing you gets to speak instead.
- It fails silently. Nothing errors, nothing 404s, and a browser test looks perfect, because a browser is the one client that does run the JavaScript.
- The elements that go missing are the expensive ones. Body text, contact details and the booking form are precisely what somebody is trying to find when they ask an assistant about a business.
The failure
"use client";
export default function Services() {
const [services, setServices] = useState([]);
useEffect(() => {
fetch("/api/services")
.then((r) => r.json())
.then(setServices);
}, []);
return <ul>{services.map((s) => <li key={s.id}>{s.name}</li>)}</ul>;
}In a browser this renders the full service list. In the HTML a non-rendering crawler receives, it is an empty ul. The data exists, the endpoint works, and the page passes every visual check. The crawler still gets nothing.
The fix
// A server component. The fetch happens before the HTML is sent,
// so the list is in the markup every client receives.
export default async function Services() {
const services = await getServices();
return <ul>{services.map((s) => <li key={s.id}>{s.name}</li>)}</ul>;
}Server render anything answer critical, or prerender it at build time. Client-side fetching is still fine for anything that is not the answer: a filter, a dashboard, a live availability widget. The rule is about what a stranger came to find out.
Verify the fix
Changing the code is not the same as fixing the problem. Confirm it.
- Disable JavaScript in your browser and reload the page. This is the thirty second version of the test and it catches most of it.
- Fetch the page with curl and a crawler user agent, then search the output for text you know is visible in a browser. A match means the content is in the HTML. No match means it is not.
- Test the pages that answer questions, not the homepage. Services, pricing, contact and location pages are where the answer-critical content lives.
- After changing to server rendering, run the same curl and confirm the string is now present. The fix is only real when the raw fetch shows it.
- Check your name, address and phone specifically. It was invisible on 14.7% of the sites we tested and it is the single element most likely to be asked for.
# 1. What a non-rendering AI crawler receives.
curl -s -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); \
compatible; GPTBot/1.1; +https://openai.com/gptbot" \
https://example.com/services > raw.html
# 2. Is the answer in there? Search for text you know is on the page.
grep -c "Emergency callout" raw.html
# 3. Compare against the rendered DOM.
npx -y puppeteer-cli screenshot --help >/dev/null 2>&1
node -e '
const puppeteer = require("puppeteer");
(async () => {
const b = await puppeteer.launch();
const p = await b.newPage();
await p.goto("https://example.com/services", {waitUntil: "networkidle0"});
const html = await p.content();
require("fs").writeFileSync("rendered.html", html);
await b.close();
})();'
# 4. The gap between these two numbers is your exposure.
grep -c "Emergency callout" rendered.htmlStep 2 returning 0 while step 4 returns 1 or more is the finding. You do not need the full study to check one page. Strings worth testing: a service name, your phone number, the heading above your contact form.
Exceptions and misconceptions
This is not the same as being blocked
Blocking is a robots.txt decision somebody made. This is an accidental consequence of a rendering choice. The two have different fixes and are frequently confused, which is why they are separate lessons.
Googlebot rendering JavaScript does not mean AI crawlers do
Google operates a rendering service and a second wave of indexing. That is Google's infrastructure, not a property of crawlers in general. Assuming every crawler behaves like Googlebot is the specific assumption this lesson exists to correct.
Managed builders did better, not worse
Wix came in at 2.9% and Webflow at 6.7%, against 26.6% for WordPress and 31.8% for custom or other. This inverts the usual folk wisdom. The reason is not builder quality, it is that those platforms server render answer-critical content by default. A custom build that server renders is just as safe, and a WordPress site loading its content over the REST API is just as exposed.
Client-side rendering is not a defect by itself
Interactive state, filters, dashboards and anything behind a login can stay client side. Nothing is lost when a crawler cannot see a sort control. The rule applies to the content that answers the question a stranger arrived with.
Watch it
Primary sources
- OpenAI: GPTBot and how it crawls
- Google Search Central: understand JavaScript SEO basics
- MDN: server-side versus client-side rendering
The measurement above comes from our study, AI Visibility Study 2026.
Related lessons
Check your own site for this
DeepAudit AI renders your page in a real browser and reports the affected code, so you can see exactly where each finding came from. Free, no signup.
Run a free auditLast reviewed 2026-09-18. Checks covered: .