DEV Community

Cover image for JSON-LD for AI Search: 7 Mistakes That Block Citations
A.R. Hashir
A.R. Hashir

Posted on AI-assisted

JSON-LD for AI Search: 7 Mistakes That Block Citations

Most structured data audits end the same way: the validator says zero errors, the Rich Results Test passes, and six months later the site has still never appeared in a single ChatGPT or Perplexity answer.

I have gone through this with enough sites now to say it plainly — the problem is almost never syntax. It is that the markup describes a page when the retrieval system is looking for an entity, and the page's actual content never reaches the crawler in the first place.

Here are the seven failures I see repeatedly, in the order they cost you the most.


1. You described the page, not the entity

The default pattern is a single WebPage block. That tells a machine "this is a page of the type WebPage" — which is roughly as useful as telling it "this is a file."

Retrieval and grounding systems need to resolve who you are, not what tag wraps the URL. There is no path from WebPage to a knowledge graph node.

Before

{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": "Acme Analytics — Product Page",
  "url": "https://acme.dev/product"
}
Enter fullscreen mode Exit fullscreen mode

After

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://acme.dev/#organization",
      "name": "Acme Analytics",
      "url": "https://acme.dev/",
      "logo": {
        "@type": "ImageObject",
        "url": "https://acme.dev/logo-512.png"
      },
      "sameAs": [
        "https://github.com/acme-analytics",
        "https://www.crunchbase.com/organization/acme-analytics",
        "https://en.wikipedia.org/wiki/Acme_Analytics"
      ]
    },
    {
      "@type": "SoftwareApplication",
      "@id": "https://acme.dev/product#app",
      "name": "Acme Analytics",
      "applicationCategory": "BusinessApplication",
      "operatingSystem": "Web",
      "publisher": { "@id": "https://acme.dev/#organization" },
      "description": "Privacy-first web analytics with cookieless session tracking.",
      "url": "https://acme.dev/product"
    },
    {
      "@type": "WebPage",
      "@id": "https://acme.dev/product#webpage",
      "url": "https://acme.dev/product",
      "isPartOf": { "@id": "https://acme.dev/#website" },
      "about": { "@id": "https://acme.dev/product#app" }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The @id values are the actual work here. They turn three disconnected blocks into one addressable graph, so any node can be referenced from anywhere on the site without repeating the data.


2. Missing sameAs, or sameAs pointing at dead profiles

sameAs is the cheapest disambiguation signal that exists and it is the one most often skipped.

An LLM asked "what is Acme Analytics" has no way to know your domain is the authority on Acme Analytics. It does know what a GitHub org, a Crunchbase entry, and a Wikidata item are. sameAs is how you hand it the bridge.

Rules that matter here:

  • Only link profiles that actually exist and are populated. A 404, a private repo, or a profile with no bio is worse than no link — it's a failed verification.
  • Wikipedia and Wikidata beat everything. If you have either, that's your strongest edge.
  • 3–8 links is the sweet spot. Forty links look like spam and get discounted.
  • Never invent one. A broken sameAs is a trust signal in the wrong direction.

3. Your JSON-LD is injected after hydration

This is the quiet killer on Next.js, Nuxt, React SPAs, and anything using a head-management library.

If this is how your schema gets onto the page:

// ❌ invisible to most AI crawlers
useEffect(() => {
  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.text = JSON.stringify(schema);
  document.head.appendChild(script);
}, [schema]);
Enter fullscreen mode Exit fullscreen mode

...then the crawler has to execute JavaScript and wait for hydration and wait for your effect to flush before the markup exists. Googlebot will sometimes manage it. Most AI crawlers will not.

The fix is boring: serialize the JSON server-side and render it as static text in the document.

// ✅ present in the raw HTML response
const jsonLd = JSON.stringify(schema, null, 2);
return (
  <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: jsonLd }}
  />
);
Enter fullscreen mode Exit fullscreen mode

Verify it in the only way that counts — not in DevTools, but in the actual response:

curl -s https://acme.dev/product | grep -c 'application/ld+json'
Enter fullscreen mode Exit fullscreen mode

If that returns 0 while the browser shows valid schema, you've found your bug.


4. Your content only exists after JavaScript runs

Same root cause as #3, much worse consequence. Schema is metadata. Content is what gets embedded and cited.

If your product copy, documentation body, or article text renders client-side, you are shipping an empty shell to every crawler that doesn't run a browser.

# Compare what a crawler sees vs what a user sees
curl -s -A "GPTBot" https://acme.dev/docs/getting-started | wc -w
curl -s https://acme.dev/docs/getting-started | wc -w
Enter fullscreen mode Exit fullscreen mode

A 30-word difference is a widget. A 900-word difference means your documentation does not exist as far as retrieval is concerned — no matter how good your JSON-LD is.

The practical answer is SSR, SSG, or ISR for every route you want cited. Client-rendered routes are for app state, not published content.


5. Structurally valid, semantically broken values

Validators check shape, not meaning. These all pass and all degrade the output:

Value Broken Fixed
datePublished "Oct 24, 2026" "2026-10-24" (ISO 8601)
image "/cover.png" "https://acme.dev/cover.png"
@type "Webpage" "WebPage"
@type "Article" with trailing space "Article"
name `"Acme Analytics Best Tools"`
description 400-character paragraph 150–160 chars, one sentence

ISO dates matter more than they look. "Oct 24, 2026" is ambiguous enough that some parsers drop the field entirely, and dateModified is one of the few freshness signals you fully control.


6. FAQPage used as a keyword dump

Two separate mistakes get made here constantly.

Mistake A — malformed structure. mainEntity must be an array of Question objects, each with a nested acceptedAnswer of type Answer. A flat {"question": "...", "answer": "..."} is not Schema.org.

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does JSON-LD improve AI search visibility?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "JSON-LD does not directly improve rankings, but it removes ambiguity about what a page and its publisher are, which helps retrieval systems select the page as a source."
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Mistake B — mismatched pairs. The question in the markup has to be answered by the text that follows it on the page. If the marked-up question says "How much does it cost?" and the visible answer is about onboarding, you have created a false pair. That is worse than no markup, because now the system has a confident wrong answer attributed to you.

One honest caveat: Google restricted FAQ rich results to authoritative government and health sites back in 2023, and removed HowTo rich results entirely. So this markup will not give you a SERP feature. What it still does is produce clean, self-contained question-answer chunks that retrieval pipelines can pull without parsing your surrounding prose. That's the reason to keep it — not the rich result.


7. Conflicting entity data across pages

Bolt on a plugin, add a second one later, and you end up with two Organization blocks per page saying different things:

{ "@type": "Organization", "name": "Acme Ltd" }
{ "@type": "Organization", "name": "Acme Analytics Inc.", "url": "https://www.acme.dev" }
Enter fullscreen mode Exit fullscreen mode

To a reconciling system this is two companies. Consolidation fails silently and you get no error anywhere.

Pick canonical values once — exact legal name, exact canonical URL, exact logo — and use them in <title>, og:title, and every Organization node. Then dedupe:

# Should return exactly 1 per page
curl -s https://acme.dev/ | grep -o '"@type":"Organization"' | wc -l
Enter fullscreen mode Exit fullscreen mode

The 5-minute audit order

If you only do five things after reading this:

  1. curl your three most important URLs and confirm the body copy is there without JS
  2. Count application/ld+json blocks in the raw HTML — zero means everything else is moot
  3. Check every sameAs URL returns 200 and has real content on it
  4. Confirm dateModified is ISO 8601 and actually recent
  5. Search for "@type":"Organization" and make sure it appears once, with consistent values

Notice that none of these involve the Rich Results Test. It tells you the markup is well-formed, which was never the question.


FAQ

Does adding JSON-LD get my site cited by ChatGPT?
Not on its own. Citation depends on whether the page is crawlable, whether its content is retrievable without JavaScript, and whether it is the clearest available answer to the question. Schema removes ambiguity about the publisher and the entity; it does not create content authority.

Should I block GPTBot?
GPTBot is OpenAI's training crawler. It is separate from OAI-SearchBot, which handles ChatGPT search, and ChatGPT-User, which fires when a user asks ChatGPT to fetch a URL. Blocking GPTBot does not remove you from ChatGPT search results. Blocking OAI-SearchBot does. Make that decision deliberately rather than copying a robots.txt snippet from a blog post.

Does FAQPage markup still do anything?
Not for rich results on most sites. It still produces clean question-answer pairs that are trivial to chunk during retrieval, which is a real but modest benefit.

How long until structured data shows an effect?
Recrawl cadence is the limiter, not the markup. Crawlers revisit popular pages within days and low-traffic pages within weeks to months. Expect a quarter, not a week, and track server-log bot hits rather than trying to infer it from traffic.


If you want the full version of this audit as a single document — all seven checks expanded into a 100-point readiness list, plus the complete JSON-LD templates for Organization, SoftwareApplication, Article, Person, and FAQPage graphs, already wired with @id cross-references — I put it together here: *Structured for Answers →***

Next in this series: How to Test If AI Crawlers Can Actually Read Your Site — the eight checks that tell you where you actually stand before you change a single line of markup.

Top comments (1)

Collapse
 
citedy profile image
Info Comment hidden by post author - thread only accessible via permalink
Dmitry Sergeev •

We need to produce a short YouTube comment, casual, about this video specifically. Should mention something like "I kept making mistake #3..." or ask a question about a mistake. Must be short, one sentence, maybe two. No quotes, no hashtags, no markdown. Must be in plain text. So produce something like: "wow, mistake #4 about missing @id really tripped me up, anyone else had that issue?" This meets guidelines: starts with lowercase, casual, question, specific to video. Check:

Some comments have been hidden by the post's author - find out more