Structured Data That Machines Read
10 min read · updated August 4, 2026
Structured data is the one place where you tell a machine what your page means rather than hoping it infers it. It is also where honest sites quietly stop being honest, and where a mechanical detail — that JSON-LD lives inside a <script> element — decides whether half the systems you care about ever see it.
Three encodings, one vocabulary
schema.org is a vocabulary of types and properties. It can be expressed three ways in a page, and the choice matters more than it looks.
| Encoding | Description |
|---|---|
| JSON-LD | A JSON object in a <script type="application/ld+json"> element. Separate from the visible markup, so it survives a redesign. The recommended form, and the one with the mechanical caveat in the next section. |
| Microdata | itemscope, itemtype and itemprop attributes on the visible elements themselves. Cannot drift from what is displayed, because it is attached to it. Verbose, and painful to keep correct across a component library. |
| RDFa | The same idea with a different attribute set, from the wider linked-data world. Fully supported by the major parsers and rarely chosen for new work. |
Use JSON-LD unless you have a specific reason not to. It is generated from the same variables your template already has, which is the only arrangement in which the markup and the page cannot disagree.
Why JSON-LD is invisible to a text extractor
Here is the mechanism nobody writes down, and it is tier-2 observable in one command.
A structured-data parser looks for script elements of type application/ld+json and reads their contents. An HTML-to-text converter — the kind that sits in the fetch stage of a retrieval pipeline, turning a fetched page into something to chunk — does the opposite: it strips script and style elements first, precisely because their contents are not prose and would poison the text with JavaScript. Your JSON-LD is discarded before the text is ever chunked.
Prove it on your own page in one line, with any HTML-to-text tool:
# Does the structured data survive text extraction? curl -sS https://example.com/some-page > page.html # What a structured-data parser sees: grep -c 'application/ld+json' page.html # What a text extractor sees (python-readability, trafilatura, # lynx -dump, or your own stripper) — search the output for a value # that appears ONLY in the JSON-LD: lynx -dump -nolist page.html | grep -c 'a-value-only-in-your-jsonld'
If the second count is zero, the conclusion is not that structured data is useless. It is that structured data reaches parsers, and visible text reaches extractors, and you should not rely on one to carry a fact the other needs. Every fact that matters must be in the prose. The markup is a second, machine-typed statement of facts the page already makes.
Microdata behaves differently here, because its values are the visible text — an extractor keeps them by definition. That is not a reason to switch, but it is the reason the rule below is not merely an ethical preference.
The rule that keeps markup honest
One rule, and it makes several other decisions for you: nothing in the structured data may be a fact the page does not also show a human. Every value passed to the generator should be the same variable the visible markup renders.
What that rules out, concretely:
- An
aggregateRatingwith no reviews on the page. The most abused property in the vocabulary and the one most likely to earn a manual action. - A
priceValidUntilyou have not committed to. If you cannot honour the date, it is not a fact. - FAQ markup for questions not on the page. The guidance is explicit that the question and answer text must both be visible.
- An
authornobody wrote. If the page has no byline, the author is the organisation, and saying so is more credible than inventing a person. - An
imagethat does not illustrate the article. Pointing a thousand articles at one site-wide mark is a claim that each illustrates itself; omitting the property is more honest and costs nothing.
The rule also gives you an implementation test: if your generator takes an argument that is not already on the page, that argument is the bug.
The types worth the effort
These types and properties all exist in the vocabulary and are widely consumed. Pick by what your page is, not by what sounds impressive.
| Type | Description |
|---|---|
| Article / TechArticle / BlogPosting | For prose. TechArticle is an Article subtype and is the accurate choice for documentation and technical explainers. Carries headline, description, datePublished, dateModified, author, publisher, inLanguage, timeRequired. |
| BreadcrumbList | The trail you already render, as ListItem entries with position and item. Cheap, unambiguous, and it tells a parser your site's shape without it inferring one from your navigation. |
| Organization | Your identity, on one page, with sameAs pointing at the profiles that are also you. This is the property that ties your identifiers together and it matters for entity resolution — see the page on false claims about your company. |
| WebSite | Site-level name and URL, usually with the Article's isPartOf pointing at it. |
| CollectionPage + ItemList | For a hub that lists other pages. The ItemList should be the same list in the same order the page renders, which is the point of it. |
| Product / Offer | For something purchasable, with price and priceCurrency. Only where the price is on the page and current. |
| SoftwareApplication | For a tool or an app: applicationCategory, operatingSystem, and a zero-price Offer where it is genuinely free. Do not describe a calculator as an Article. |
| Dataset | For downloadable data, with distribution, license and creator. Under-used, and it is the type most likely to make a data page findable by something that wants data. |
| FAQPage / QAPage | Question and Answer entries. Still valid vocabulary and still parsed, but see the section on rich results before assuming it earns a display change. |
| citation / isBasedOn / license | Properties rather than types, and the most underused ones here. They state what a page rests on and what may be done with it, in machine-readable form. |
A complete article block
Generated from the page’s own fields, with no value invented. The escaping detail in the last line is not optional: a string in your data containing a closing script tag would end the element early, which is the one genuine injection risk in a block like this.
function articleJsonLd(page, siteUrl) {
const url = `${siteUrl}/learn/${page.slug}`;
return {
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": url,
mainEntityOfPage: { "@type": "WebPage", "@id": url },
headline: page.title, // the visible <h1>
description: page.summary, // the meta description
datePublished: page.published, // the date the URL began to exist
dateModified: page.updated, // the visible "updated" line
timeRequired: `PT${page.minutes}M`, // the visible "N min read"
inLanguage: "en",
author: { "@type": "Organization", name: "Example", url: siteUrl },
publisher: { "@type": "Organization", name: "Example", url: siteUrl },
};
}
// Rendering it. The escape matters.
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
}}
/>timeRequired takes an ISO 8601 duration, so “6 min read” is PT6M. datePublished and dateModified take ISO dates or datetimes. Getting the format wrong is the most common validator error and it is silent otherwise.
What stopped producing rich results
Two changes from 2023 are still being sold as tactics and are worth knowing precisely, because they are tier-1 facts you can read in the operator’s own documentation.
- FAQ rich results were restricted. Google announced in August 2023 that the FAQ rich result would generally be shown only for well-known authoritative government and health sites. The markup is still valid and still parsed; the display change is what went away for everyone else.
- HowTo rich results were removed. Announced in the same change, limited and then dropped as a search feature.
The lesson generalises past those two: a display feature is a product decision and can be withdrawn, while the vocabulary is a standard and does not disappear. Mark up your page because the markup is a true, structured statement of what the page contains. If you are marking it up for a specific visual treatment, you have bought an asset with an unannounced expiry date.
Validating it, and testing extraction
- Validate the vocabulary. The Schema Markup Validator at
validator.schema.orgchecks your JSON against schema.org itself, with no opinion about any search product. This is the one that tells you whether your types and properties are real. - Check the search-product view. Google’s Rich Results Test tells you whether a specific feature is eligible, which is a narrower and more volatile question.
- Validate the rendered page, not the source. If your markup is injected client-side, paste the URL rather than the source — or better, move the block to the server response, because a non-rendering fetcher will never run the injection at all. See JavaScript rendering and what crawlers see.
- Diff the markup against the page. Take every value in the JSON and confirm the string appears in the visible text. A twenty-line script that does this in CI is the cheapest possible defence against the whole class of dishonest-markup problems.
- Run the extraction test from the second section, so you know which of your facts survive to the text a chunker sees.
What structured data does not do
It is not a ranking factor in the sense people mean, and no assistant operator documents using it to choose sources. Claims that adding JSON-LD raises your citation rate are tier 3 under the scale in what is real and what is sold — unverifiable at your scale and unsupported by any public documentation.
What it does do is cheap, real and independent of that question: it removes ambiguity for parsers that read it, it makes your dates, authorship and identity machine-readable, and it forces you to write down what your page actually claims to be. That is worth an afternoon. It is not worth a retainer.