Table of contents
If your JSON-LD is added by a Google Tag Manager Custom HTML tag, a plain HTTP fetch of your page contains zero schema blocks. Google can still read it, because Googlebot renders JavaScript. Crawlers that do not run scripts see a page with no structured data at all. Moving the block into the page template takes about 10 minutes per template and turns that count from 0 to 1.
This guide proves the problem on a page you can reproduce, then walks the move on four stacks. It covers check C3 (title, meta, JSON-LD and a single H1 in the served HTML).
Quick summary
| If you want to... | Do this | Time |
|---|---|---|
| Find out whether GTM is your problem | Run curl -s https://yoursite.com/ | grep -c '<script type="application/ld+json"'. Zero means the block is not in the served HTML | 1 minute |
| Keep Google rich results | Nothing breaks: Google documents Tag Manager as a supported route | 0 |
| Make the block visible to non-rendering crawlers | Move it into the template's <head>, then pause the GTM tag | 10 minutes |
| Confirm the fix | npx aiscan-cli yoursite.com, then read check C3 | 2 minutes |
According to Google's structured data documentation, Google Search "can understand and process structured data that's available in the DOM when it renders the page," and the page lists Google Tag Manager as one of the two most common ways to generate JSON-LD with JavaScript. That is a statement about Google. The AI crawlers are a different question.
Why a Tag Manager block disappears
A Custom HTML tag runs in the browser after the container loads. It creates a <script type="application/ld+json"> element and appends it to the page. The server never sent that element. Anything that reads the raw response, which is what a crawler without a JavaScript engine does, never sees it.
OpenAI's crawler documentation lists three agents: OAI-SearchBot and GPTBot, which are robots.txt tokens, and ChatGPT-User, which "may visit a web page" when a user asks a question. The page does not say whether any of them executes JavaScript, so this guide makes no claim about it either way.
Schema App, a structured data vendor that sells both client-side and server-side deployment, hedges in its own words: Googlebot and Bingbot "can reliably render JavaScript," while for ChatGPT and Claude "it is likely that they are unable to render and index JSON-LD that is inserted client-side." Its recommendation is to put JSON-LD "in the initial HTML." The hedge is worth keeping: "likely" is not a measurement. A block in the served HTML is correct under every version of the answer. A block added by a tag is correct under only one.
The test: same block, two delivery routes
I built two pages with an identical Organization block. Page 1 injects it from a script that runs after the load event, the same mechanism a Custom HTML tag uses (it is not a live GTM container; the mechanism is the point). Page 2 has the block in the template <head>. Fetched from a local server on 28 October 2026 and rendered in Chromium from the pre-installed Playwright build.
| Fetch method | Page 1: injected after load | Page 2: in the template |
|---|---|---|
curl default user agent, count of ld+json script tags | 0 | 1 |
curl with a GPTBot user agent string | 0 | 1 |
| Headless Chromium, nodes in the rendered DOM | 1 | 1 |
Two things stand out. Rendering fixes page 1, which is why Google's tools show a green result for tag-injected schema. And the user agent string changes nothing: the server sent the same bytes to every caller, so the GPTBot row only proves that no server-side trick was involved. One trap in the test itself: the inline script contains the text application/ld+json, so a naive grep -c 'ld+json' returns 1 on page 1 and looks like a pass. Grep for the full <script type="application/ld+json" opening tag.
I did not record an AIScan C3 result for the two test pages, because AIScan scans public URLs and the pages were served locally. Run the scan on your own site after the move; the check is described on the content checks page.
Move the block server-side
Pick the row that matches your stack. Each one puts the same <script type="application/ld+json"> block in the HTML the server sends.
| Stack | Where the block goes | Notes |
|---|---|---|
| WordPress | Your theme's <head>, through the wp_head action, or your SEO plugin's schema settings | ThinkRank manages schema from one plugin and migrates from Rank Math, Yoast, AIOSEO and SEOPress. |
| Shopify | Online Store > Themes > Edit code, then layout/theme.liquid inside <head> | StoreSEO generates store schema. According to Shopify's layout docs, layout/theme.liquid is the layout that wraps every page |
| Next.js | A <script type="application/ld+json"> rendered by a Server Component | Next.js has an official JSON-LD guide. Do not set it inside a useEffect |
| Static site | A head include or the base layout | Any generator that writes it at build time works |
WordPress
- Check whether your theme or SEO plugin already prints an Organization block: view source and search for
application/ld+json. - If it prints one, delete the Tag Manager copy instead of adding a second. Two blocks for one entity is its own defect.
- If it prints none, add the block through ThinkRank or a small function hooked to
wp_head, so it is part of the page response.
Shopify
- Open Online Store > Themes > Edit code and open
layout/theme.liquid. - Paste the
<script type="application/ld+json">block above</head>. - Use Liquid objects for values that change per store, such as
{{ shop.name }}, and wrap them in thejsonfilter so quotes are escaped.
Next.js
- Render the block in a Server Component (the default in the App Router). According to the Next.js guide, that is the supported pattern.
- Escape
<as<when you serialize, which the same guide recommends against script injection. - Build and
curlthe production output. If the count is 0, the block ended up in a Client Component.
Static sites
Put the block in the layout file that wraps every page, rebuild, and check the generated HTML in your output folder.
Then retire the tag
In Google Tag Manager, pause the Custom HTML tag after the template block is live. Leaving both produces two Organization blocks for one entity. Keep the old tag's version history so you can restore it.
Who does not need to move: if a plugin or theme already prints the block in the HTML, you have nothing to do. GTM is also fine for blocks that depend on a visitor, such as a logged-in state, which no crawler needs.
Verify it worked
Lead with the scan:
npx aiscan-cli yoursite.com
Read check C3. It should report a JSON-LD block in the served HTML. By hand:
curl -s https://yoursite.com/ | grep -c '<script type="application/ld+json"'
Expected output: 1 for one block, or the number of blocks you intend. Then repeat with -A "GPTBot"; the number must match. If it is still 0, view source in the browser (not the inspector, which shows the rendered DOM): the block is in a Client Component, a cached page, or a template other than the one serving that URL.
Re-run the check after theme updates. A theme update can overwrite theme.liquid, and a CDN can keep serving the old page until you purge it.
Next steps
Run the scan above and read C3. Which types to add is covered in JSON-LD schema types for AI. If your whole page is built by JavaScript, read why Next.js and React sites are invisible to AI crawlers, and use the curl method for fetching your site as an agent to see what a crawler receives. Every fix guide is listed on the guides page.
Frequently asked questions
How do I check whether my JSON-LD comes from Google Tag Manager?
View the page source in the browser (not the inspector) and search for application/ld+json. If the block shows in the inspector but not in view-source, or curl returns 0, a script adds it after load.
My Rich Results Test passes but curl shows no JSON-LD. Which one is right?
Both. Google's test renders the page, and Google documents that it processes structured data in the rendered DOM. Curl shows what a crawler without a JavaScript engine receives. The block is real for Google and absent for them.
I get 1 from grep on a page I know has no schema in the HTML. Why?
Your grep matched script text that mentions application/ld+json, such as a GTM snippet or a tag that creates the element. Grep for the full opening tag, <script type="application/ld+json", so only real blocks count.
After moving the block, curl still returns 0. What went wrong?
Check three things: the block is in a Client Component or a useEffect (Next.js), a page cache or CDN still serves the old HTML, or you edited a template other than the one that renders that URL. View source on the live URL to confirm.
Search Console now reports duplicate or conflicting structured data. What do I do?
You have the template block and the Tag Manager block at once. Pause the Custom HTML tag in the container, publish the container, and wait for the next crawl.
Do I have to remove Google Tag Manager?
No. Keep it for analytics and marketing tags. Only the tag that creates the JSON-LD needs to move into the template.
Does moving JSON-LD out of GTM change my Google rich results?
It should not, because Google reads the block either way. Compare the Rich Results Test before and after, and check that values such as name and URL are identical.
Which crawlers run JavaScript?
Check each operator's own documentation. Google documents rendering. OpenAI's crawler page does not state it, and Schema App says ChatGPT and Claude are likely not to render. A block in the served HTML does not depend on the answer.

About the author
M Asif Rahman
Entrepreneur, curious about AI
I have been building companies for a long time, and lately most of my curiosity goes to AI. AIScan is a small personal project I built on a flight from Dubai to Narita. I use it to understand how ready the web is for AI agents, and I share what the scans teach me.
Related guides
AI readiness setup guide by platform (2026)
Verified 17, 18, 20 and 22 October 2026. Updated on 22 October 2026. An AIready site needs six things: a robots.txt that names AI crawlers D1, B2, a sitemap D2, a declared feed E5, an llms.txt C2,…
Content Signals in robots.txt: 117 of 799 Sites Declare One
Of 799 sites in our scan database on 2 November 2026, 117 serve a robots.txt with a ContentSignal line 14.6%. Among the 677 sites that serve a usable robots.txt at all, that is 17.3%. Another 682…
Does robots.txt Need a Sitemap Line? 782 Sites Show What Happens
Of 782 sites in our scan database on 29 October 2026, 572 serve a robots.txt that declares a sitemap, 89 serve a robots.txt with no Sitemap line, and 121 fail the robots.txt check outright. Declaring…
