
{"id":1373,"date":"2026-07-16T06:34:52","date_gmt":"2026-07-16T06:34:52","guid":{"rendered":"https:\/\/maxaeo.ai\/blog\/javascript-schema-ai-crawlers\/"},"modified":"2026-07-16T06:34:52","modified_gmt":"2026-07-16T06:34:52","slug":"javascript-schema-ai-crawlers","status":"publish","type":"post","link":"https:\/\/maxaeo.ai\/blog\/javascript-schema-ai-crawlers\/","title":{"rendered":"JavaScript Schema and AI Crawlers: Is Your JSON-LD Actually Retrieved?"},"content":{"rendered":"<p><strong>Short version: if JavaScript writes your structured data into the page after load, most AI crawlers never see it.<\/strong> That is the JavaScript schema AI crawlers problem in one sentence\u2014and it quietly breaks structured data on a large share of modern sites. GPTBot, ClaudeBot, and PerplexityBot fetch your raw HTML and move on. They do not run your scripts, so any JSON-LD added by a tag manager, a <code>useEffect<\/code> hook, or a hydration step simply isn&#39;t there when they read the page.<\/p>\n<p>This matters because structured data is one of the clearest ways to tell an answer engine what your page is, who published it, and how it connects to your brand. If that signal exists only in the rendered DOM, you are optimizing for a reader that never arrives. Below: how to check what is actually retrieved, why tag managers and hydration fail, and how to fix it\u2014grounded in crawler-traffic data and a controlled test you can reproduce.<\/p>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" style=\"max-width:100%;height:auto\" loading=\"lazy\"  src=\"https:\/\/maxaeo.ai\/blog\/wp-content\/uploads\/2026\/07\/1784132991931-3-91934-1.jpg\" alt=\"Diagram of the JavaScript schema AI crawlers retrieval path: raw HTML fetch, render pass, and JSON-LD parsing\"><\/figure>\n<h2>The short answer: most AI crawlers can&#39;t see JavaScript-injected schema<\/h2>\n<p><strong>Most AI crawlers do not execute JavaScript.<\/strong> They read the HTML your server returns and nothing more. If your JSON-LD is added client-side, it is absent from what they retrieve\u2014so it cannot inform how ChatGPT, Claude, or Perplexity describe your brand.<\/p>\n<p><a href=\"https:\/\/vercel.com\/blog\/the-rise-of-the-ai-crawler\" target=\"_blank\" rel=\"noopener\">Vercel&#39;s analysis of AI crawler traffic<\/a> found that none of the major AI crawlers\u2014OpenAI&#39;s GPTBot, OAI-SearchBot, and ChatGPT-User; Anthropic&#39;s ClaudeBot; and PerplexityBot\u2014render JavaScript. They sometimes <em>fetch<\/em> script files (GPTBot in <strong>11.50%<\/strong> of requests, ClaudeBot in <strong>23.84%<\/strong>) but never <em>execute<\/em> them, so they can&#39;t read client-rendered content. The meaningful exception is Google: Gemini and Google&#39;s AI surfaces lean on Googlebot&#39;s infrastructure, which renders JavaScript on a deferred pass.<\/p>\n<p>So the rule of thumb is blunt. <strong>For everything except Google&#39;s stack, treat AI crawlers as non-rendering.<\/strong> Whatever isn&#39;t in the raw HTML doesn&#39;t exist to them.<\/p>\n<h2>What &quot;retrieved&quot; actually means: the three gates your JSON-LD must pass<\/h2>\n<p>&quot;Retrieved&quot; is not one step\u2014it is three. For structured data to help an AI engine, it must clear three gates: <strong>Retrieval<\/strong> (present in the raw bytes), <strong>Rendering<\/strong> (built into the DOM), and <strong>Reconciliation<\/strong> (valid and parseable). JavaScript-injected schema often clears the last two but fails the first\u2014which is exactly why it looks fine in a browser and vanishes for crawlers.<\/p>\n<p>This three-gate model is the fastest way to reason about a specific bot. A non-rendering crawler can only ever clear Gate 1. Googlebot can clear all three\u2014but on a delay, and never guaranteed for every URL. <strong>Design so Gate 1 always passes, and every downstream engine works.<\/strong><\/p>\n<h3>Gate 1 \u2014 Retrieval: is it in the raw bytes?<\/h3>\n<p>Retrieval is whether the <code>&lt;script type=&quot;application\/ld+json&quot;&gt;<\/code> block is physically present in the HTTP response body, before any JavaScript runs. This is the only gate that non-rendering crawlers clear. If your schema isn&#39;t in these bytes, GPTBot, ClaudeBot, and PerplexityBot see nothing.<\/p>\n<h3>Gate 2 \u2014 Rendering: is it in the DOM after JS runs?<\/h3>\n<p>Rendering is whether the block appears once scripts execute and the DOM is built. A tag-manager tag or a hydration effect that injects JSON-LD passes here\u2014which is why it looks correct in your browser&#39;s Elements panel and misleads teams into thinking the job is done.<\/p>\n<h3>Gate 3 \u2014 Reconciliation: is it valid and parseable?<\/h3>\n<p>Reconciliation is whether the JSON is well-formed, uses correct <code>@type<\/code> values, and references entities that resolve. A block can render perfectly and still fail here\u2014duplicate <code>@id<\/code>s, a trailing comma, or a hydration mismatch that ships two conflicting <code>Article<\/code> objects. Clearing Gate 1 is wasted if Gate 3 breaks.<\/p>\n<h2>Which AI crawlers render JavaScript? A decision matrix<\/h2>\n<p>Here is how the major crawlers behave, and whether client-side JSON-LD reaches them. Use it to decide how much you can rely on JavaScript for a given surface.<\/p>\n<table>\n<thead>\n<tr>\n<th>Crawler<\/th>\n<th>Operator<\/th>\n<th>Renders JavaScript?<\/th>\n<th>Sees JS-injected JSON-LD?<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GPTBot<\/td>\n<td>OpenAI<\/td>\n<td>No<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>OAI-SearchBot \/ ChatGPT-User<\/td>\n<td>OpenAI<\/td>\n<td>No<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>ClaudeBot<\/td>\n<td>Anthropic<\/td>\n<td>No<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>PerplexityBot<\/td>\n<td>Perplexity<\/td>\n<td>No<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>Googlebot (feeds AI Overviews, AI Mode, Gemini grounding)<\/td>\n<td>Google<\/td>\n<td>Yes (deferred render)<\/td>\n<td>Eventually, after render<\/td>\n<\/tr>\n<tr>\n<td>Bingbot (feeds Microsoft Copilot)<\/td>\n<td>Microsoft<\/td>\n<td>Limited<\/td>\n<td>Unreliable<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Two nuances matter. <strong>Google rendering is not a safety net.<\/strong> It runs on a second, delayed pass subject to crawl budget, and the engines that build their own answers\u2014ChatGPT, Claude, Perplexity\u2014largely rely on their own non-rendering crawlers, not Googlebot. If you want to see which index each assistant actually draws from, our breakdown of <a href=\"https:\/\/maxaeo.ai\/blog\/which-search-engines-power-ai-answers\">which search index powers each AI engine<\/a> maps it engine by engine. Treat any crawler not proven to render as if it can&#39;t.<\/p>\n<h2>How to test whether your JSON-LD is actually retrieved<\/h2>\n<p><strong>The fastest test is to fetch your page the way a non-rendering crawler does and search the raw response for your schema.<\/strong> If it isn&#39;t there, AI crawlers can&#39;t see it. You need no special access\u2014just a terminal.<\/p>\n<ol>\n<li><strong>Fetch the raw HTML.<\/strong> Run <code>curl -sL https:\/\/example.com\/your-page | grep -i &quot;application\/ld+json&quot;<\/code>. Servers return the same bytes to any client, so plain <code>curl<\/code> already shows what a non-rendering bot receives.<\/li>\n<li><strong>Confirm with a crawler user agent.<\/strong> Repeat with <code>curl -A &quot;GPTBot&quot; -sL ...<\/code> to rule out user-agent-specific delivery. If the result differs, you are cloaking by UA\u2014fix that separately.<\/li>\n<li><strong>Read the block, not just its presence.<\/strong> Use <code>grep -A 30 &quot;application\/ld+json&quot;<\/code> to print the JSON and eyeball whether it is complete or truncated.<\/li>\n<li><strong>Render the page and compare.<\/strong> Load the same URL in <a href=\"https:\/\/search.google.com\/test\/rich-results\" target=\"_blank\" rel=\"noopener\">Google&#39;s Rich Results Test<\/a> in URL mode, which renders like Googlebot. Schema that appears here but not in step 1 is JavaScript-injected\u2014visible to Google eventually, invisible to non-rendering AI crawlers now.<\/li>\n<li><strong>Validate the markup.<\/strong> Paste the block into the <a href=\"https:\/\/validator.schema.org\/\" target=\"_blank\" rel=\"noopener\">Schema.org validator<\/a> to clear Gate 3.<\/li>\n<\/ol>\n<p>If step 1 is empty and step 4 shows the schema, the diagnosis is confirmed: your structured data lives only in the DOM. For a deeper, repeatable version of this method across content types, see our <a href=\"https:\/\/maxaeo.ai\/blog\/can-ai-crawlers-render-javascript\">repeatable test for whether AI crawlers render JavaScript<\/a>.<\/p>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" style=\"max-width:100%;height:auto\" loading=\"lazy\"  src=\"https:\/\/maxaeo.ai\/blog\/wp-content\/uploads\/2026\/07\/1784132991931-3-91934-2.jpg\" alt=\"Terminal output comparing curl with a GPTBot user agent against a rendered DOM snapshot for a JSON-LD block\"><\/figure>\n<h3>What our controlled test found<\/h3>\n<p>To make the failure concrete, we built one article page in Next.js and injected the <strong>same<\/strong> <code>Article<\/code> JSON-LD three ways: hardcoded in the server HTML, added via a client-side <code>useEffect<\/code> hook, and pushed through a Google Tag Manager custom-HTML tag. We then fetched the URL with a non-rendering user agent and compared it against the Googlebot-rendered DOM.<\/p>\n<table>\n<thead>\n<tr>\n<th>Injection method<\/th>\n<th>In raw HTML (non-rendering crawler)?<\/th>\n<th>In rendered DOM (Googlebot)?<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Hardcoded in server HTML<\/td>\n<td>Yes<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>useEffect<\/code> client hook<\/td>\n<td>No<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>GTM custom-HTML tag<\/td>\n<td>No<\/td>\n<td>Yes (URL mode only)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Only the server-rendered variant appeared in the raw bytes. Both JavaScript methods were invisible to the non-rendering fetch and surfaced only once the page was rendered. <strong>The takeaway is unambiguous: for retrieval, injection method\u2014not markup quality\u2014decides whether AI crawlers ever see your schema.<\/strong> This holds for every schema type\u2014<code>Organization<\/code>, <code>Product<\/code>, <code>FAQPage<\/code>, <code>BreadcrumbList<\/code>\u2014because retrieval depends on where the markup lives, not on what <code>@type<\/code> it declares.<\/p>\n<h2>Tag manager failure modes<\/h2>\n<p><strong>Google Tag Manager is the most common way JSON-LD goes missing.<\/strong> GTM is a JavaScript container that fires after the page loads, so anything it injects lives only in the rendered DOM. Non-rendering AI crawlers receive the HTML before GTM runs and never see the tag.<\/p>\n<p>The failure has several flavors worth naming:<\/p>\n<ul>\n<li><strong>Custom-HTML tag timing.<\/strong> The tag fires on DOM Ready or a later trigger\u2014well after the initial byte stream a crawler reads.<\/li>\n<li><strong>Consent-mode gating.<\/strong> If schema injection waits for a consent event that a bot never triggers, the tag may not fire at all.<\/li>\n<li><strong>Debug-only success.<\/strong> GTM Preview and the Rich Results Test <em>URL mode<\/em> both render, so the block looks present. That rendered view masks the raw-HTML reality a non-rendering crawler actually reads.<\/li>\n<\/ul>\n<p>Google&#39;s own documentation on <a href=\"https:\/\/developers.google.com\/search\/docs\/appearance\/structured-data\/generate-structured-data-with-javascript\" target=\"_blank\" rel=\"noopener\">generating structured data with JavaScript<\/a> confirms Googlebot can process JS-generated markup, but still recommends placing JSON-LD in the source. For AI visibility, &quot;Google can eventually render it&quot; is the wrong bar\u2014the assistants your buyers use don&#39;t render at all.<\/p>\n<h2>Hydration and framework failure modes<\/h2>\n<p>Client-side frameworks introduce a subtler class of failures where the schema is in your code but not in the response. These are easy to miss because everything works in development and in the browser.<\/p>\n<p>Common patterns we see when auditing <a href=\"https:\/\/maxaeo.ai\/blog\/schema-for-ai-search\">structured data for AI search<\/a>:<\/p>\n<ul>\n<li><strong><code>useEffect<\/code> injection.<\/strong> Adding the <code>&lt;script&gt;<\/code> inside an effect guarantees it is client-only. It never reaches the server-rendered HTML.<\/li>\n<li><strong>Streaming and Suspense flush order.<\/strong> In the Next.js App Router, schema placed inside a <code>Suspense<\/code> boundary or a streamed segment can flush <em>after<\/em> the initial HTML\u2014so a non-rendering crawler reads the shell without it. The markup is valid; it just arrives too late for a bot that never waits.<\/li>\n<li><strong>Hydration mismatches.<\/strong> If the server and client render different markup, React can discard the server node during hydration, quietly removing schema that <em>was<\/em> in the raw bytes.<\/li>\n<li><strong>Double injection.<\/strong> A component that mounts twice (or a layout plus a page both emitting <code>Article<\/code>) ships conflicting entities, failing Gate 3.<\/li>\n<\/ul>\n<p>The through-line: <strong>framework convenience often trades away retrieval.<\/strong> If schema depends on the client to exist, it is a client-only signal. The same visibility trap affects other JS-dependent patterns\u2014content behind <a href=\"https:\/\/maxaeo.ai\/blog\/can-ai-crawlers-see-hidden-content\">tabs and accordions<\/a> can disappear from crawlers for the very same reason.<\/p>\n<h2>The fix: put schema into the raw HTML<\/h2>\n<p><strong>The fix is one rule: the <code>application\/ld+json<\/code> block must exist in the HTTP response before any JavaScript runs.<\/strong> In practice that means rendering it server-side or at build time, then proving it survived.<\/p>\n<ul>\n<li><strong>Server-side render (SSR) the schema.<\/strong> Emit the JSON-LD in your server component or template so it ships in the initial bytes. Next.js, Nuxt, Astro, and SvelteKit all support this in their SSR or static modes.<\/li>\n<li><strong>Static-generate (SSG) where content is stable.<\/strong> Prebuild the page\u2014schema included\u2014so every request serves complete HTML.<\/li>\n<li><strong>Prerender legacy SPAs.<\/strong> If you can&#39;t move off client rendering, prerender the routes that need structured data so bots get a fully-formed snapshot.<\/li>\n<li><strong>Retire GTM for schema.<\/strong> Move JSON-LD out of the tag manager and into the template. Keep GTM for analytics, not structured data.<\/li>\n<\/ul>\n<p>Choosing between these is the <a href=\"https:\/\/maxaeo.ai\/blog\/ssr-vs-csr-ai-crawlers\">server-side vs client-side rendering<\/a> decision applied to one asset. Whichever route you take, re-run the raw-HTML <code>curl<\/code> check afterward. <strong>The only proof the fix worked is the schema showing up in the bytes, not in the browser.<\/strong> Retrievable evidence is what earns AI citations; invisible markup earns none.<\/p>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" style=\"max-width:100%;height:auto\" loading=\"lazy\"  src=\"https:\/\/maxaeo.ai\/blog\/wp-content\/uploads\/2026\/07\/1784132991931-3-91934-3.jpg\" alt=\"Server-side rendered page source showing an application\/ld+json script present in the raw HTML bytes\"><\/figure>\n<h2>A pre-publish verification checklist<\/h2>\n<p>Before you ship a page that depends on structured data, confirm each gate. This routine prevents the most common regression in generative engine optimization work.<\/p>\n<ol>\n<li><code>curl<\/code> the URL and confirm the JSON-LD is in the raw response (Gate 1).<\/li>\n<li>Fetch with <code>GPTBot<\/code> and a generic UA\u2014confirm identical output (no cloaking).<\/li>\n<li>Open the Rich Results Test in URL mode\u2014confirm it renders and detects the type (Gate 2).<\/li>\n<li>Validate the JSON in the Schema.org validator (Gate 3).<\/li>\n<li>Check for duplicates: exactly one primary entity per page unless intentionally nested.<\/li>\n<li>After any deploy, re-run step 1\u2014hydration and build changes silently break retrieval.<\/li>\n<\/ol>\n<p>Bake this into release QA and into ongoing AI search monitoring so a framework upgrade never quietly deletes your schema. A tool that tracks how assistants describe your brand will flag the downstream symptom\u2014your entity going fuzzy in ChatGPT or Perplexity\u2014but the raw-HTML check is what tells you <em>why<\/em>.<\/p>\n<h2>Frequently asked questions<\/h2>\n<h3>Do AI crawlers read JSON-LD at all?<\/h3>\n<p>Yes\u2014when it is in the raw HTML. GPTBot, ClaudeBot, and PerplexityBot parse the HTML your server returns, including inline <code>application\/ld+json<\/code> blocks. What they can&#39;t do is execute JavaScript, so any schema added after load is invisible to them. Placement, not format, is the deciding factor.<\/p>\n<h3>Is schema injected by Google Tag Manager visible to ChatGPT?<\/h3>\n<p>No. GTM fires client-side JavaScript after the page loads, and ChatGPT&#39;s crawlers don&#39;t render JavaScript. The tag exists only in the rendered DOM, which those bots never build. Move the JSON-LD into your server-rendered HTML if you want it retrieved.<\/p>\n<h3>Will Google still see my JavaScript-injected schema?<\/h3>\n<p>Usually yes, but on a delay. Googlebot renders JavaScript on a deferred pass, so client-injected schema can reach Google Search and AI Overviews eventually. That doesn&#39;t help ChatGPT, Claude, or Perplexity, which rely on their own non-rendering crawlers. Rendering by one engine is not visibility across all of them.<\/p>\n<h3>Does server-side rendering guarantee my schema is retrieved?<\/h3>\n<p>SSR gets the block into the raw bytes, which clears the retrieval gate for every crawler. It does not guarantee the markup is valid or free of duplicates. Always finish with a validator check and a post-deploy <code>curl<\/code> to confirm nothing stripped it during hydration.<\/p>\n<h3>Which schema types does this affect?<\/h3>\n<p>All of them. The retrieval problem is about <em>where<\/em> the markup lives, not <em>what<\/em> it declares\u2014so <code>Organization<\/code>, <code>Product<\/code>, <code>FAQPage<\/code>, <code>BreadcrumbList<\/code>, and <code>Article<\/code> are equally at risk. If any of them is injected client-side, non-rendering crawlers miss it.<\/p>\n<h3>Can I just serve my schema as a separate static .json file?<\/h3>\n<p>Not for discovery. Search and AI crawlers look for JSON-LD inline in the page&#39;s HTML, not by guessing at a standalone file. Embed the <code>&lt;script type=&quot;application\/ld+json&quot;&gt;<\/code> in the server-rendered document so it travels with the page a crawler already requests.<\/p>\n<p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"Article\",\n  \"headline\": \"JavaScript Schema and AI Crawlers: Is Your JSON-LD Actually Retrieved?\",\n  \"description\": \"When JavaScript injects your schema, AI crawlers like GPTBot and ClaudeBot may never retrieve the JSON-LD\u2014here's how to test what's actually seen and fix it.\",\n  \"image\": \"image-placeholder\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"MaxAEO\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"MaxAEO\",\n    \"logo\": {\n      \"@type\": \"ImageObject\",\n      \"url\": \"image-placeholder\"\n    }\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\/\/maxaeo.ai\/blog\/javascript-schema-ai-crawlers\"\n  },\n  \"datePublished\": \"2026-07-15\",\n  \"dateModified\": \"2026-07-15\"\n}\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>When JavaScript injects your schema, AI crawlers like GPTBot and ClaudeBot may never retrieve the JSON-LD. Test what&#8217;s actually seen\u2014and fix it.<\/p>\n","protected":false},"author":1,"featured_media":1370,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1373","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/posts\/1373","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/comments?post=1373"}],"version-history":[{"count":0,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/posts\/1373\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/media\/1370"}],"wp:attachment":[{"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/media?parent=1373"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/categories?post=1373"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/maxaeo.ai\/blog\/wp-json\/wp\/v2\/tags?post=1373"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}