AIScan fix guide cover: Schema in GTM, missing from the HTML, with Google and OpenAI logos and a broken page turning into a fixed one.
AIScan fix guide cover: Schema in GTM, missing from the HTML, with Google and OpenAI logos and a broken page turning into a fixed one.
AI Readiness

JSON-LD in Google Tag Manager is missing from your served HTML: test it and move it server-side

A Google Tag Manager JSON-LD block never reaches a plain HTTP fetch. See the curl and Chromium test, then move it into your template on four stacks.

28 Oct 2026 7 min read

This guide covers C3 · Content — for WordPress, Shopify, Next.js / Vercel.

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 thisTime
Find out whether GTM is your problemRun curl -s https://yoursite.com/ | grep -c '<script type="application/ld+json"'. Zero means the block is not in the served HTML1 minute
Keep Google rich resultsNothing breaks: Google documents Tag Manager as a supported route0
Make the block visible to non-rendering crawlersMove it into the template's <head>, then pause the GTM tag10 minutes
Confirm the fixnpx aiscan-cli yoursite.com, then read check C32 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 methodPage 1: injected after loadPage 2: in the template
curl default user agent, count of ld+json script tags01
curl with a GPTBot user agent string01
Headless Chromium, nodes in the rendered DOM11

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.

StackWhere the block goesNotes
WordPressYour theme's <head>, through the wp_head action, or your SEO plugin's schema settingsThinkRank manages schema from one plugin and migrates from Rank Math, Yoast, AIOSEO and SEOPress.
ShopifyOnline 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.jsA <script type="application/ld+json"> rendered by a Server ComponentNext.js has an official JSON-LD guide. Do not set it inside a useEffect
Static siteA head include or the base layoutAny generator that writes it at build time works

WordPress

  1. Check whether your theme or SEO plugin already prints an Organization block: view source and search for application/ld+json.
  2. If it prints one, delete the Tag Manager copy instead of adding a second. Two blocks for one entity is its own defect.
  3. 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

  1. Open Online Store > Themes > Edit code and open layout/theme.liquid.
  2. Paste the <script type="application/ld+json"> block above </head>.
  3. Use Liquid objects for values that change per store, such as {{ shop.name }}, and wrap them in the json filter so quotes are escaped.

Next.js

  1. Render the block in a Server Component (the default in the App Router). According to the Next.js guide, that is the supported pattern.
  2. Escape < as < when you serialize, which the same guide recommends against script injection.
  3. Build and curl the 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.

M Asif Rahman

About the author

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

Browse every guide by check and platform