Skip to content
← All articles

Schema.org for AI Search: Types, @id and Common Errors

Short answer. Schema.org markup does not raise rankings and does not "connect" your site to AI. It makes a page unambiguous to a machine: it names the entity type, the author, the dates, and the links between nodes via @id. Three things decide whether it helps in AI answers: the markup matches the visible text, the nodes form one connected graph, and there are no duplicates. Markup that contradicts the page hurts more than no markup at all.

What markup gives an AI engine, and what it does not

Every page describes itself through two channels. The first is the visible text: headings, paragraphs, tables, lists. It works everywhere, for every consumer, with no exceptions. The second is JSON-LD inside <script type="application/ld+json">. It only works where somebody reads it — and that is a much narrower set of places than most teams assume.

Split the consumers by how they actually obtain your page:

  • Index-backed engines. The answer is assembled on top of a search index. Structured data reaches that index: it is parsed by the same pipeline that prepares rich results. Here your JSON-LD is almost certainly read.
  • Assistant-owned indexes. A bot crawls the site, stores the HTML, and puts it into an internal index. The markup is stored along with everything else — but no vendor publishes which fields are used at answer time, so treat the benefit as plausible rather than proven.
  • Live fetch by link. A user hands a URL to an assistant, which downloads the page and converts HTML to text. Here everything depends on the extractor, and typical content-cleaning libraries drop the contents of script elements entirely — JSON-LD included.

The third case leads to a practical rule that saves weeks of work: if a fact matters, it must live in the visible text. Update date, author name, price, the steps of a procedure, units of measure. Markup duplicates and disambiguates those facts for machines; it never substitutes for them. How assistants turn HTML into text is covered in how AI crawlers read sites and in the content extractability guide.

Markup is not a ranking lever. It is a way to remove ambiguity. A page with no markup but clean text and a sane structure gets cited more often than a page with perfect JSON-LD and a mess in the markup.

Crawler access is a separate topic with its own decisions — it is covered in the companion article on robots.txt and AI bots. Markup is worthless if the bot was never let onto the page in the first place.

Two description channels of a page: visible text and JSON-LD, and three classes of consumers
Visible text is read by every consumer; JSON-LD only by some of them. Hence the rule: anything important is duplicated in the text.

Which markup types actually affect extractability

The Schema.org vocabulary has more than eight hundred types. Fewer than ten of them matter on an ordinary website. The rest either have no support among consumers or describe entities you simply do not have.

Article, BlogPosting, TechArticle — attribution and freshness

Article is the base type; BlogPosting and TechArticle are its subtypes. The difference is not cosmetic: a subtype states the genre more precisely, which is exactly what separates a step-by-step technical piece from a general opinion post. The mandatory minimum for any of them:

  • headline — matches the visible page heading. Google has historically recommended keeping it short (around a hundred characters); a long headline is truncated.
  • datePublished and dateModified — ISO 8601 with a time zone offset, not a bare date string invented by the template.
  • author — a Person or Organization node with its own @id and url, not a plain string.
  • publisher — a reference to the organization node.
  • mainEntityOfPage — a reference to the WebPage node, so that the article and the page are not read as two unrelated entities.

How to phrase headings and descriptions is its own discipline — see the guide on title and description tags.

HowTo and FAQPage — the most quotable and the most dangerous

Both types hand the model ready-made fragments: numbered steps and question-answer pairs. That is literally the shape an assistant wants to emit, which is why such blocks get quoted more eagerly than prose.

An honest caveat: per Google's 2023 announcements, FAQ rich results were narrowed to a small set of authoritative sites and HowTo rich results were wound down. That is not a reason to strip the markup — the machine-readable structure remains, and a rich snippet was never the only goal. But you should no longer expect FAQPage to buy you decorations in the search results page.

The single most common mistake in the wild is always the same: FAQPage sits on a page that has no FAQ, because a CMS template emitted it "just in case". That is a direct violation of structured data policies, and it is trivially detectable.

Product, Offer, AggregateRating — where manual-action risk starts

Product plus Offer is normal markup for a product page: price, currency, availability, offer validity. Trouble starts with AggregateRating. An aggregated rating with no real, visible reviews on the page is one of the most common triggers for manual actions, and it is one of the easiest things for a reviewer to verify by hand.

A telling case from practice: the homepage of one project carried an aggregateRating with pleasant numbers — a score and a review count that were backed by no visible publication anywhere. The validator stayed silent about it, because the JSON was perfectly well-formed. The block was removed during an audit. Marking up nothing is safer than marking up something the page cannot show.

Organization, WebSite, BreadcrumbList — the skeleton of the site

These three differ from the rest: they describe the site itself rather than the text of one page, so they need no content backing.

  • Organization — the brand node: name, url, logo, and sameAs pointing at verifiable profiles. Every article attaches to it through publisher.
  • WebSite — the site node; pages attach to it through isPartOf.
  • BreadcrumbList — the hierarchy; the one type that is nearly always worth adding to every nested page.
TypeWhere it belongsWhat it gives a machineWhat you risk
Article / BlogPosting / TechArticleArticles, guides, newsAuthor, dates, genre, source attributionheadline that disagrees with the H1; stamped dates
HowToStep-by-step instructionsSteps as an ordered listMore steps in the markup than on the page
FAQPageA real question-answer blockReady pairs for verbatim citationMarkup with no visible FAQ is a policy violation
Product / OfferProduct pagesPrice, currency, availabilityPrice in markup differs from price on the page
AggregateRatingOnly with visible reviewsA summary scoreManual action for an invented rating
OrganizationEvery page (one node)Brand identity, sameAsDifferent @id per page reads as different brands
WebSiteEvery pageBinds pages to one siteDuplicate nodes across several blocks
BreadcrumbListAny nested pageHierarchy and section contextposition order disagrees with visible breadcrumbs

What AI engines read from markup, and what only search robots read

There is no public specification describing which engine reads which field — no vendor publishes one, and anyone claiming otherwise is guessing. What you can reason about is the access mechanism, and that turns out to be enough for design decisions.

MechanismHow the page is obtainedDoes it see JSON-LDWhat decides the outcome
Search indexRegular crawl by a search robotYes — parsed by the indexing pipelinePrecise types, a connected graph, sameAs
Assistant-owned indexCrawl by an AI vendor bot, HTML storedProbably, but usage is undocumentedComplete HTML without JS rendering, bot access
Live fetch by linkA user pasted the URL into an assistantOften not: the extractor drops script tagsVisible text, headings, block order
Training crawlBulk download into a corpusData enters the corpus wholesaleGuarantees nothing for any specific answer
Design your markup as if only half of your consumers will read it. Then the other half loses nothing — and the first half gets a bonus.

Why @id matters and why scattered nodes are worse than linked ones

@id is a stable identifier for a node in the graph. A node without @id is anonymous: a parser cannot tell that the author of this article and the person on the "about the author" page are the same human, or that the publisher here and the publisher on the next page are one organization.

Three consequences of skipping @id:

  1. Duplicate entities. Every page mints a fresh "organization" with the same name. Instead of one strong node you get a hundred weak ones with no relation between them.
  2. No reuse. The publisher is redescribed inside every block, and the fields drift apart over time: an old logo here, a differently punctuated name there.
  3. Broken brand consolidation. Without a shared @id and sameAs, a machine has no basis for treating scattered mentions as one entity.

The correct shape is a single JSON-LD block per page with a @graph container, where nodes reference each other by @id:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Example",
      "url": "https://example.com/",
      "logo": {
        "@type": "ImageObject",
        "@id": "https://example.com/#logo",
        "url": "https://example.com/logo.png",
        "width": 512,
        "height": 512
      },
      "sameAs": [
        "https://www.wikidata.org/wiki/Q00000000"
      ]
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com/",
      "name": "Example",
      "publisher": { "@id": "https://example.com/#organization" },
      "inLanguage": "en-US"
    },
    {
      "@type": "WebPage",
      "@id": "https://example.com/guide/schema/#webpage",
      "url": "https://example.com/guide/schema/",
      "name": "Structured data for AI search",
      "isPartOf": { "@id": "https://example.com/#website" },
      "breadcrumb": { "@id": "https://example.com/guide/schema/#breadcrumb" },
      "datePublished": "2026-03-04T09:00:00+00:00",
      "dateModified": "2026-08-11T14:20:00+00:00"
    },
    {
      "@type": "TechArticle",
      "@id": "https://example.com/guide/schema/#article",
      "headline": "Structured data for AI search",
      "description": "Which Schema.org types affect extractability.",
      "datePublished": "2026-03-04T09:00:00+00:00",
      "dateModified": "2026-08-11T14:20:00+00:00",
      "author": { "@id": "https://example.com/team/jdoe/#person" },
      "publisher": { "@id": "https://example.com/#organization" },
      "mainEntityOfPage": { "@id": "https://example.com/guide/schema/#webpage" },
      "proficiencyLevel": "Expert",
      "inLanguage": "en-US"
    },
    {
      "@type": "Person",
      "@id": "https://example.com/team/jdoe/#person",
      "name": "Jane Doe",
      "url": "https://example.com/team/jdoe/",
      "jobTitle": "SRE",
      "worksFor": { "@id": "https://example.com/#organization" }
    },
    {
      "@type": "BreadcrumbList",
      "@id": "https://example.com/guide/schema/#breadcrumb",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home",
          "item": "https://example.com/" },
        { "@type": "ListItem", "position": 2, "name": "Guides",
          "item": "https://example.com/guide/" },
        { "@type": "ListItem", "position": 3, "name": "Structured data for AI search" }
      ]
    }
  ]
}

Conventions worth fixing in your team once and never revisiting:

  • @id is an absolute URL with a fragment: #organization, #website, #webpage, #article, #person, #breadcrumb. The fragment keeps it unique and human-readable at the same time.
  • The organization identifier is one per site and never changes. Changing that @id is equivalent to announcing a new brand.
  • A repeat reference to a node is just { "@id": "..." } with no field duplication.
  • One block with @graph beats six independent blocks: relations inside a single block are explicit, relations across blocks are not.
Entity graph: organization, website, page, article, author and breadcrumbs linked through @id
A connected graph: one organization node that the site, pages and articles attach to. Without @id you get a scatter of anonymous nodes instead of a graph.

What a broken graph looks like: a real case

On one project we reviewed, a catalogue page carried two CollectionPage nodes. They were emitted by two different parts of the template — a shared layout and a section block. The names differed, and neither node had an @id:

[
  {
    "@context": "https://schema.org",
    "@type": "CollectionPage",
    "name": "Tools",
    "url": "https://example.com/tools/"
  },
  {
    "@context": "https://schema.org",
    "@type": "CollectionPage",
    "name": "All website checks — the full tool catalogue",
    "url": "https://example.com/tools/"
  }
]

Formally this is not an error, and a validator waves it through in green. In practice a parser receives two entities with the same URL and different names, and has to choose which description is the real one. Neither variant is reinforced — they compete with each other. The fix is dull: collapse to a single node with an @id, and take the name from the same source that renders the visible H1.

A second case from the same family: every blog article was typed as Article. That is valid and safe, which is exactly why it survives for years. But Article is the most featureless choice available — it does not distinguish a step-by-step procedure from an opinion piece or a reference note. After the type detection was rewritten — guides to TechArticle, procedures to HowTo, commentary to BlogPosting — the data finally carried a genre distinction that had simply not existed before.

A validator does not catch semantic errors. Two nodes without @id, a featureless type, a modification date taken from build time — all of it passes green and all of it degrades your data.

Common markup errors: symptom, cause, check, fix

SymptomCauseHow to checkFix
Validator is green, effect is zeroMarkup is not backed by visible textCompare headline with H1, fields with page contentDrive both from one data source in the template
Two entities for one pageSeveral blocks with no @id from different template partsDump all nodes with the command in the next sectionOne block with @graph, an @id on every node
FAQ markup does nothingFAQPage on a page with no visible FAQSearch the page HTML for the question textEither remove the markup or add a real block
dateModified is always todayThe date comes from build or request timeCompare with the Last-Modified header and edit historyEmit the date of the actual text change
The whole block is ignoredInvalid JSON: trailing comma, smart quotes from a visual editorRun it through a parser (see the CI gate below)Never edit JSON-LD in a WYSIWYG field
Author is not recognised"author": "Jane Doe" as a string instead of a nodeCheck the value type of the fieldA Person node with @id and url
Validator sees zero nodesMarkup is injected by client-side JavaScriptCompare raw HTML from curl with the browser DOMRender the markup server-side
Brand reads as several different onesDifferent organization @id per page, no sameAsCompare the organization @id across three or four pagesOne @id and one sameAs set for the whole site
Markup vanished after a releaseThe template was reissued without the block and nobody noticedA check in CI on every deployA gate that fails the build on an empty graph
Breakdown of typical markup errors: duplicate nodes, markup without content, stamped dates
Three errors a validator treats as normal: duplicates without @id, FAQPage without an FAQ, and dateModified taken from build time.

How to validate: commands and validators

Start with what the server actually returns, not with what the browser shows. The gap between the two is the single most common reason behind "the markup is there, but nobody sees it".

Dump every node on a page with its type and identifier:

curl -sSL -A 'Mozilla/5.0 (compatible; audit/1.0)' https://example.com/guide/schema/ \
  | python3 -c '
import sys, re, json
html = sys.stdin.read()
pat = r"<script[^>]*application/ld\+json[^>]*>(.*?)</script>"
blocks = re.findall(pat, html, re.S | re.I)
print("blocks:", len(blocks))
for i, b in enumerate(blocks, 1):
    try:
        data = json.loads(b)
    except json.JSONDecodeError as e:
        print(i, "BROKEN JSON:", e)
        continue
    nodes = data.get("@graph", [data]) if isinstance(data, dict) else data
    for n in nodes:
        print(i, n.get("@type"), "|", n.get("@id") or "NO @id")
'

If you get blocks: 0 while the browser clearly shows markup, JavaScript is drawing it. For any bot that does not execute scripts, that markup does not exist.

Cross-check the heading and the dates between markup, visible text, and response headers:

# the visible H1
curl -sSL "$URL" | tr -d '\n' | grep -o '<h1[^>]*>[^<]*' | sed 's/.*>//'

# headline straight from the markup
curl -sSL "$URL" | python3 -c '
import sys, re, json
html = sys.stdin.read()
for b in re.findall(r"ld\+json[^>]*>(.*?)</script", html, re.S):
    d = json.loads(b)
    for n in (d.get("@graph", [d]) if isinstance(d, dict) else d):
        if "headline" in n:
            print(n["headline"], "|", n.get("dateModified"))
'

# what the server itself claims about freshness
curl -sSI "$URL" | grep -i '^last-modified'

And a CI gate that fails the build when a page ships broken JSON or nodes without identifiers:

curl -sSL "$URL" | python3 -c '
import sys, re, json
html = sys.stdin.read()
bad = 0
found = 0
for b in re.findall(r"ld\+json[^>]*>(.*?)</script", html, re.S):
    try:
        d = json.loads(b)
    except Exception as e:
        print("BROKEN JSON:", e)
        bad += 1
        continue
    for n in (d.get("@graph", [d]) if isinstance(d, dict) else d):
        found += 1
        if "@id" not in n:
            print("NO @id:", n.get("@type"))
            bad += 1
if found == 0:
    print("NO STRUCTURED DATA AT ALL")
    bad += 1
sys.exit(1 if bad else 0)
' && echo "schema OK"

Two external tools are worth keeping in the loop: the Schema.org validator, which checks conformance to the vocabulary itself without any search-engine opinion attached, and Google's rich results test, which reports only on features Google supports. The first is stricter about the vocabulary, the second about specific snippet requirements. They do not replace each other, and a green result in one says nothing about the other.

How to check it with enterno.io

  • Schema.org markup checker — pulls every JSON-LD block off the page and shows types, identifiers and syntax errors exactly as a parser would see them.
  • AI readiness check — evaluates the page end to end: bot access, structure, markup, presence of a content map.
  • SEO audit — headings, a single H1, canonical URLs, meta descriptions.
  • HTTP header checkLast-Modified, caching, redirects. If the page answers with an unexpected status, the markup reaches nobody.
  • robots.txt checker — confirm the page is open to bots at all.
  • Monitoring — so that markup disappearing after a release surfaces immediately, not a month later in a report.

What markup does not do

The list is short, and it is worth reading before you budget the work:

  • It does not raise rankings. Structured data is not a ranking factor. It changes presentation and comprehension, not the weight of a document.
  • It does not guarantee a rich result. Showing enhanced results is a search engine's decision, not a consequence of well-formed JSON-LD.
  • It does not replace text. If a fact is missing from the page, it is missing for every consumer that drops script content.
  • It does not protect content. Neither from copying nor from use in model training — that belongs to robots.txt and server-side rules.
  • It does not fix inaccessibility. A page blocked in robots.txt, a 403 served to a bot, or markup drawn by client-side JS all reduce the work to zero.
  • It does not buy "a slot in AI results". No such slot exists: the answer is assembled from scratch for every query.
If the task is phrased as "add markup so we appear in AI answers", the task is phrased wrong. The correct phrasing is "make the page unambiguous to a machine, and make sure the machine can reach it".
Pre-deploy markup checklist: validity, agreement with visible text, one connected graph
The pre-deploy check: valid JSON, every field backed by visible text, all nodes linked into one graph.

Pre-deploy checklist

  • Markup is JSON-LD, served by the server in the raw HTML, not drawn by a script.
  • One block per page, with a @graph inside.
  • Every node has an @id; the organization identifier is identical across all pages.
  • The primary type matches the genre of the page instead of being the default fallback.
  • headline matches the visible H1; the description does not contradict the opening paragraph.
  • datePublished and dateModified are real ISO 8601 dates, not build timestamps.
  • author and publisher are nodes, not strings, and the author has a page on the site.
  • Every field is backed by visible content: no FAQ markup without an FAQ, no rating without reviews, no steps without steps.
  • The JSON is valid, and the check runs in CI and fails the build.
  • The page is open to bots and returns 200 without JS rendering.

FAQ

Is markup required to appear in an AI answer?

No. Assistants cite pages with no markup at all — clean text and sane headings are enough. Markup raises the chance that you are understood correctly: genre, author, date, brand ownership. It is about the quality of comprehension, not about admission.

JSON-LD, microdata, or RDFa?

JSON-LD. It is decoupled from the layout, so it survives redesigns, it validates easily, and it reuses nodes through @id. Microdata clings to specific tags and dies at the first template rewrite. RDFa is rare on ordinary sites and less well supported.

How many markup blocks can a page have?

Technically any number — parsers collect them together. Practically, one block with @graph is more reliable: relations between nodes inside one block are explicit, while several blocks emitted from different template parts are a direct route to duplicates without @id.

Is FAQPage still worth it if rich results barely show up?

Yes, if the FAQ genuinely exists on the page. Question-answer pairs remain the most quotable format regardless of whether a search engine draws a snippet. If there is no FAQ, the markup must not be there.

What goes into dateModified?

The date the text actually changed. A date that shifts on every rebuild destroys the signal: if "updated today" is true for every page on the site, it no longer separates updated material from untouched material.

Does sameAs help?

It is the one cheap way to say explicitly that the brand on your site and a profile elsewhere are the same entity. The links in sameAs must point to pages you genuinely own and that can be verified back to you; an arbitrary list of links is not a signal.

Does markup injected by a script count?

Only for consumers that execute JavaScript. Search robots do it with deferred rendering; most AI crawlers do not. Treat client-side markup as non-existent and you will be right more often than wrong.

Where to go next: structured data and SEO for classic search and rich results; how to check your markup for a step-by-step audit; robots.txt and AI bots for crawler access; llms.txt for a content map; the AI readiness checklist and GEO for the strategic layer.

Check the Schema.org markup of a page →

Check your website right now

Audit your site's SEO →
More articles: SEO
SEO
Website Migration Checklist: Avoid SEO and Downtime Pitfalls
16.03.2026 · 401 views
SEO
Sitemap XML: Structure, Limits, Generation and Validation
16.03.2026 · 363 views
SEO
robots.txt Guide: Syntax, Rules, Testing and Ready-Made Files
16.03.2026 · 337 views
SEO
Subdomain vs Subdirectory for SEO: Which Structure Wins?
16.03.2026 · 303 views