---
title: "JSON-LD in Google Tag Manager is missing from your served HTML: test it and move it server-side"
slug: json-ld-google-tag-manager-ai-crawlers
published: 2026-10-28T13:24:38.533878+00:00
updated: 2026-10-28T13:24:38.533878+00:00
author: "M Asif Rahman"
author_url: https://ar.bd
category: "AI Readiness"
tags: check:C3, platform:wordpress, platform:shopify, platform:nextjs
description: "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."
url: https://aiscan.site/blog/json-ld-google-tag-manager-ai-crawlers
---

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](/docs/checks/content) (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](https://developers.google.com/search/docs/appearance/structured-data/generate-structured-data-with-javascript), 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](https://platform.openai.com/docs/bots) 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](https://support.schemaapp.com/support/solutions/articles/33000296861-large-language-models-and-javascript-consumption): 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](/docs/checks/content).

## 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](https://thinkrank.ai) 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](https://storeseo.com/) generates store schema. According to [Shopify's layout docs](https://shopify.dev/docs/storefronts/themes/architecture/layouts), `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](https://nextjs.org/docs/app/guides/json-ld). 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

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:

```bash
npx aiscan-cli yoursite.com
```

Read check **C3**. It should report a JSON-LD block in the served HTML. By hand:

```bash
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](/blog/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](/blog/nextjs-react-invisible-to-ai-crawlers), and use [the curl method for fetching your site as an agent](/blog/fetch-site-as-ai-agent-curl) to see what a crawler receives. Every fix guide is listed on the [guides page](/guides).

