DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Structured Data That Machines Read

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 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.</td> </tr> <tr> <td>Microdata</td> <td>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.</td> </tr> <tr> <td>RDFa</td> <td>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.</td> </tr> </tbody></table> <p>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.</p> <h2> <a name="why-jsonld-is-invisible-to-a-text-extractor" href="#why-jsonld-is-invisible-to-a-text-extractor" class="anchor"> </a> Why JSON-LD is invisible to a text extractor </h2> <p>Here is the mechanism nobody writes down, and it is tier-2 observable in one command.</p> <p>A structured-data parser looks for <code>script</code> elements of type <code>application/ld+json</code> 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 <code>script</code> and <code>style</code> 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.</p> <p>Prove it on your own page in one line, with any HTML-to-text tool:<br> </p> <div class="highlight"><pre class="highlight shell"><code><span class="c"># Does the structured data survive text extraction?</span> curl <span class="nt">-sS</span> https://example.com/some-page <span class="o">&gt;</span> page.html <span class="c"># What a structured-data parser sees:</span> <span class="nb">grep</span> <span class="nt">-c</span> <span class="s1">'application/ld+json'</span> page.html <span class="c"># What a text extractor sees (python-readability, trafilatura,</span> <span class="c"># lynx -dump, or your own stripper) — search the output for a value</span> <span class="c"># that appears ONLY in the JSON-LD:</span> lynx <span class="nt">-dump</span> <span class="nt">-nolist</span> page.html | <span class="nb">grep</span> <span class="nt">-c</span> <span class="s1">'a-value-only-in-your-jsonld'</span> </code></pre></div> <p></p> <p>If the second count is zero, the conclusion is not that structured data is useless. It is that <strong>structured data reaches parsers, and visible text reaches extractors, and you should not rely on one to carry a fact the other needs.</strong> Every fact that matters must be in the prose. The markup is a second, machine-typed statement of facts the page already makes.</p> <p>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.</p> <h2> <a name="the-rule-that-keeps-markup-honest" href="#the-rule-that-keeps-markup-honest" class="anchor"> </a> The rule that keeps markup honest </h2> <p>One rule, and it makes several other decisions for you: <strong>nothing in the structured data may be a fact the page does not also show a human.</strong> Every value passed to the generator should be the same variable the visible markup renders.</p> <p>What that rules out, concretely:</p> <ul> <li> <strong>An <code>aggregateRating</code> with no reviews on the page.</strong> The most abused property in the vocabulary and the one most likely to earn a manual action.</li> <li> <strong>A <code>priceValidUntil</code> you have not committed to.</strong> If you cannot honour the date, it is not a fact.</li> <li> <strong>FAQ markup for questions not on the page.</strong> The guidance is explicit that the question and answer text must both be visible.</li> <li> <strong>An <code>author</code> nobody wrote.</strong> If the page has no byline, the author is the organisation, and saying so is more credible than inventing a person.</li> <li> <strong>An <code>image</code> that does not illustrate the article.</strong> 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.</li> </ul> <p>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.</p> <h2> <a name="the-types-worth-the-effort" href="#the-types-worth-the-effort" class="anchor"> </a> The types worth the effort </h2> <p>These types and properties all exist in the vocabulary and are widely consumed. Pick by what your page is, not by what sounds impressive.</p> <table><thead> <tr> <th>Type</th> <th>Description</th> </tr> </thead><tbody> <tr> <td>Article / TechArticle / BlogPosting</td> <td>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.</td> </tr> <tr> <td>BreadcrumbList</td> <td>The trail you already render, as ListItem entries with position and item. Cheap, unambiguous, and it tells a parser your site&#39;s shape without it inferring one from your navigation.</td> </tr> <tr> <td>Organization</td> <td>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.</td> </tr> <tr> <td>WebSite</td> <td>Site-level name and URL, usually with the Article&#39;s isPartOf pointing at it.</td> </tr> <tr> <td>CollectionPage + ItemList</td> <td>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.</td> </tr> <tr> <td>Product / Offer</td> <td>For something purchasable, with price and priceCurrency. Only where the price is on the page and current.</td> </tr> <tr> <td>SoftwareApplication</td> <td>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.</td> </tr> <tr> <td>Dataset</td> <td>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.</td> </tr> <tr> <td>FAQPage / QAPage</td> <td>Question and Answer entries. Still valid vocabulary and still parsed, but see the section on rich results before assuming it earns a display change.</td> </tr> <tr> <td>citation / isBasedOn / license</td> <td>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.</td> </tr> </tbody></table> <h2> <a name="a-complete-article-block" href="#a-complete-article-block" class="anchor"> </a> A complete article block </h2> <p>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.<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="kd">function</span> <span class="nf">articleJsonLd</span><span class="p">(</span><span class="nx">page</span><span class="p">,</span> <span class="nx">siteUrl</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">siteUrl</span><span class="p">}</span><span class="s2">/learn/</span><span class="p">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">slug</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span> <span class="k">return</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">@context</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://schema.org</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">TechArticle</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">@id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">url</span><span class="p">,</span> <span class="na">mainEntityOfPage</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">WebPage</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">@id</span><span class="dl">"</span><span class="p">:</span> <span class="nx">url</span> <span class="p">},</span> <span class="na">headline</span><span class="p">:</span> <span class="nx">page</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="c1">// the visible &lt;h1&gt;</span> <span class="na">description</span><span class="p">:</span> <span class="nx">page</span><span class="p">.</span><span class="nx">summary</span><span class="p">,</span> <span class="c1">// the meta description</span> <span class="na">datePublished</span><span class="p">:</span> <span class="nx">page</span><span class="p">.</span><span class="nx">published</span><span class="p">,</span> <span class="c1">// the date the URL began to exist</span> <span class="na">dateModified</span><span class="p">:</span> <span class="nx">page</span><span class="p">.</span><span class="nx">updated</span><span class="p">,</span> <span class="c1">// the visible "updated" line</span> <span class="na">timeRequired</span><span class="p">:</span> <span class="s2">`PT</span><span class="p">${</span><span class="nx">page</span><span class="p">.</span><span class="nx">minutes</span><span class="p">}</span><span class="s2">M`</span><span class="p">,</span> <span class="c1">// the visible "N min read"</span> <span class="na">inLanguage</span><span class="p">:</span> <span class="dl">"</span><span class="s2">en</span><span class="dl">"</span><span class="p">,</span> <span class="na">author</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Organization</span><span class="dl">"</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Example</span><span class="dl">"</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="nx">siteUrl</span> <span class="p">},</span> <span class="na">publisher</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">@type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Organization</span><span class="dl">"</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Example</span><span class="dl">"</span><span class="p">,</span> <span class="na">url</span><span class="p">:</span> <span class="nx">siteUrl</span> <span class="p">},</span> <span class="p">};</span> <span class="p">}</span> <span class="c1">// Rendering it. The escape matters.</span> <span class="o">&lt;</span><span class="nx">script</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">application/ld+json</span><span class="dl">"</span> <span class="nx">dangerouslySetInnerHTML</span><span class="o">=</span><span class="p">{{</span> <span class="na">__html</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">).</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/&lt;/g</span><span class="p">,</span> <span class="dl">"</span><span class="se">\\</span><span class="s2">u003c</span><span class="dl">"</span><span class="p">),</span> <span class="p">}}</span> <span class="sr">/</span><span class="err">&gt; </span></code></pre></div> <p></p> <p><code>timeRequired</code> takes an ISO 8601 duration, so “6 min read” is <code>PT6M</code>. <code>datePublished</code> and <code>dateModified</code> take ISO dates or datetimes. Getting the format wrong is the most common validator error and it is silent otherwise.</p> <h2> <a name="what-stopped-producing-rich-results" href="#what-stopped-producing-rich-results" class="anchor"> </a> What stopped producing rich results </h2> <p>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.</p> <ul> <li> <strong>FAQ rich results were restricted.</strong> 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.</li> <li> <strong>HowTo rich results were removed.</strong> Announced in the same change, limited and then dropped as a search feature.</li> </ul> <p>The lesson generalises past those two: <strong>a display feature is a product decision and can be withdrawn, while the vocabulary is a standard and does not disappear.</strong> 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.</p> <h2> <a name="validating-it-and-testing-extraction" href="#validating-it-and-testing-extraction" class="anchor"> </a> Validating it, and testing extraction </h2> <ol> <li> <strong>Validate the vocabulary.</strong> The Schema Markup Validator at <code>validator.schema.org</code> checks 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.</li> <li> <strong>Check the search-product view.</strong> Google’s Rich Results Test tells you whether a specific feature is eligible, which is a narrower and more volatile question.</li> <li> <strong>Validate the rendered page, not the source.</strong> 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 <a href="https://multigrid.ai/learn/javascript-seo-ai">JavaScript rendering and what crawlers see</a>.</li> <li> <strong>Diff the markup against the page.</strong> 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.</li> <li> <strong>Run the extraction test</strong> from the second section, so you know which of your facts survive to the text a chunker sees.</li> </ol> <h2> <a name="what-structured-data-does-not-do" href="#what-structured-data-does-not-do" class="anchor"> </a> What structured data does not do </h2> <p>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 <a href="https://multigrid.ai/learn/generative-engine-optimization">what is real and what is sold</a> — unverifiable at your scale and unsupported by any public documentation.</p> <p>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.</p> <h2> <a name="related" href="#related" class="anchor"> </a> Related </h2> <ul> <li> <a href="https://multigrid.ai/learn/retrieval-friendly-site">Retrieval-Friendly Site Architecture</a></li> <li> <a href="https://multigrid.ai/learn/eeat-technical-content">E-E-A-T for Technical Content, Concretely</a></li> </ul>

Top comments (0)