JSON-LD: adding structured data to a site that has none
Your page already describes itself to a person. Structured data is the same description written for a machine, in a format that does not depend on the machine guessing which heading was the product name. Three syntaxes can carry it and one of them is the only sensible choice in 2026.
The short answer
JSON-LD is the structured data syntax to use, and the other two are not worth writing: microdata entangles markup with data, and RDFa is heavier for no practical gain. Put a script tag of type application/ld+json anywhere in the document, in the head or the body, and ship it in the server response rather than injecting it with JavaScript.
Three syntaxes, and why only one of them is worth writing
Schema.org vocabulary can be expressed as microdata, as RDFa, or as JSON-LD. All three are read. They differ in where the statement lives. Microdata and RDFa are attributes hung on the elements you already render, so the description of your page is distributed across the page. JSON-LD is a separate block that says the same thing on its own.
Here is one fact, twice.
<!-- Microdata: the statement is threaded through the markup -->
<div itemscope itemtype="https://schema.org/Organization">
<h1 itemprop="name">Northgate Bindery</h1>
<img itemprop="logo" src="/logo-600.png" alt="">
<a itemprop="url" href="https://northgatebindery.com/">Home</a>
</div>
<!-- JSON-LD: the same statement, in one block, in one place -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Northgate Bindery",
"logo": "https://northgatebindery.com/logo-600.png",
"url": "https://northgatebindery.com/"
}
</script>The microdata version is shorter and it is the one to avoid. Not because it is invalid, but because of what happens to it next. A designer moves the logo out of that div and the logo property silently leaves your markup. A component library re-renders the heading without the itemprop. Nobody can review the description of the page in one place, because there is no one place. You cannot diff it, you cannot serialise it from your own data model, and you cannot tell whether it is complete without reading the whole template.
JSON-LD decouples what you say about the page from how the page is laid out. That is the entire argument, and it is enough.
One exception worth knowing: if a page is already marked up in microdata and it works, there is no urgency to convert it. Do not, however, run both syntaxes for the same thing. Two descriptions of one organisation is two organisations as far as a parser is concerned, and nothing in the page decides which is authoritative.
Where the script tag goes, and what actually matters
Anywhere in the document. A <script type="application/ld+json"> element is a data block: the browser never executes it, never renders it, and puts it wherever you left it. Parsers read the served HTML and find every such element regardless of whether it sat in <head> or at the end of <body>. Advice telling you it belongs in the head is folklore.
Three things that do matter, in order of how often they are the actual problem.
- It has to be in the response. If your block is written into the DOM by client-side JavaScript, only consumers that execute JavaScript will ever see it, and that is a much shorter list than the one that fetches HTML. Injecting JSON-LD through a tag manager is the common version of this mistake: whether your content survives without JS decides whether the markup counts.
- The type attribute is exact.
application/ld+json. Notapplication/json, not the transposedapplication/json+ldthat autocompletion loves to produce. The wrong value is not a warning, it is a block nobody looks inside. - The URLs are absolute. Inside the block,
/logo-600.pnghas no page to be relative to. Everyurl,imageand@idvalue is a full URL including the scheme and host.
Your platform may already be emitting some
Before you write a line, read what is being served. This is not a formality on the two platforms most sites run on.
WordPress running Yoast SEO or Rank Math already outputs a single connected block per page, typically covering the site, the current page, the publisher and, on a post, the article. Both plugins generate it from settings you may never have opened. If you now hand-add an Organization block, you have two organisation nodes with different identifiers, describing the same company, on the same page. The fix is to correct the plugin's settings, not to add a second voice. Shopify themes are the same story for products: most emit Product markup already, and a second app adding its own produces a duplicate rather than an improvement.
"We have no structured data" is a claim to verify, not a starting assumption.
View source on your own homepage and search for ld+json. It is a five-second check and it changes the whole task: adding a first block to a blank page is a different job from correcting a block a plugin generates on every page of the site.
See what this page already declares
Paste a URL. This reports whether any structured data came back and which types it named, which is the check to run before deciding what to add.
No signup required. Each free search audits one page, paste any URL to see it in action.
The smallest block worth shipping
If a site genuinely has nothing, the first thing to state is who publishes it and what the site is. Those two nodes are what author, publisher and breadcrumb markup all end up referring to later, so getting them stable first saves rewriting everything that points at them.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://northgatebindery.com/#organization",
"name": "Northgate Bindery",
"url": "https://northgatebindery.com/",
"logo": {
"@type": "ImageObject",
"url": "https://northgatebindery.com/logo-600.png",
"width": 600,
"height": 60
},
"sameAs": [
"https://www.linkedin.com/company/northgate-bindery",
"https://github.com/northgate-bindery"
]
},
{
"@type": "WebSite",
"@id": "https://northgatebindery.com/#website",
"url": "https://northgatebindery.com/",
"name": "Northgate Bindery",
"publisher": { "@id": "https://northgatebindery.com/#organization" }
}
]
}
</script>Read the publisher value again. It is not a repeated copy of the organisation, it is a pointer to the node declared above it. That is the mechanism that stops a site with forty templates from asserting forty slightly different versions of the same company.
The other property people fill in wrongly on their first attempt is sameAs. It asserts that this entity and the thing at that URL are the same entity, so it takes your own profiles and your own Wikidata or Wikipedia entry if one exists. A link to the industry you work in, or to a directory listing you do not control, is a category rather than an identity. That distinction is most of what the Organization type exists to get right.
Several blocks, or one @graph
Both are legal and both are read. A page may carry as many ld+json elements as you like, each with its own @context and its own type, and a parser treats the union of them as what the page says. Or one element can hold a @graph array of nodes sharing a single context, which is the shape in the sample above.
Pick by how the markup is produced rather than by preference. Separate blocks suit a template system where different partials own different statements: the layout emits the site and the publisher, the product partial emits the product, and neither needs to know the other exists. A single @graph suits a system where one function builds the whole description for a route, because it lets nodes reference each other by @id without repeating themselves and produces one thing to test.
What is not a choice is which node owns which fact. Two blocks that both declare a WebPage for the current URL, one from your theme and one from a plugin, is the duplicate problem again wearing a different hat. Count your nodes, not your script tags.
Why a blank slate is rated Critical
The honest starting point: a page with no structured data is not broken. It ranks, it renders, it gets crawled, and for many pages the markup would add nothing a good outline does not already carry. Rated on damage alone this would not be Critical.
It is Critical because of what it gates. Every other check in this pillar reads inside a block, so with no block there is nothing to validate, no properties to complete, no entity to reference and no type to get wrong. The whole group returns the same answer for the same reason, which is the signature of a prerequisite rather than a defect. The severity scale rates a check by what turns on it, and every other check in this pillar turns on this one.
The second reason is that it is the cheapest Critical on the list to clear. A missing canonical needs a decision about which URL wins. A crawl trap needs an architecture change. This needs one block of text in a template, once, and it stays fixed. A Critical you can close in an afternoon should not be sitting open.
Does this affect AI search?
Yes, though not in the way the pitch usually goes. The useful framing is disambiguation rather than ranking. A model reading your prose sees the string "Northgate Bindery". A block declaring a name, a canonical URL and a set of profiles that belong to you turns that string into a thing with edges, which is what makes the difference between being mentioned and being resolved.
The honest limits. No assistant vendor publishes what it does with schema.org data, so any number quoted at you about citation rates from structured data was invented. And adding JSON-LD to a page whose text does not answer anything will not get that page cited: the markup describes the content, it does not substitute for it. The order of work is prose first, markup second.
Where the block does earn its keep is survival. Content that reaches a model through an extractor, a feed or a scrape has usually lost its heading levels, its tables and most of its context. A JSON-LD block is a single self-contained object, so it either arrives intact or not at all, and intact means the same fields it had on your page.
Confirming the block is really being served
Fetch the page rather than inspecting it in a browser tab. curl -s piped to a search for ld+json tells you what a consumer that does not run JavaScript receives, which is the thing that was in doubt. If the block appears in devtools and not in the fetched HTML, your markup exists only for clients that execute scripts.
Then paste the URL into the Schema Markup Validator at validator.schema.org. It lists every type it can parse and does not filter by whether Google renders a feature for it, which makes it the right tool for confirming presence.
What that confirms is that a block exists and parses. Two things it does not confirm are the next two rungs of the same ladder: a block that fails to parse reports as nothing found rather than as an error, and a block missing required properties reports as valid. Both look exactly like success from here.
Questions this check raises
- Does JSON-LD have to be in the head?
- No. Google reads it from the head or the body, and position within the document makes no difference. What does matter is that it is present in the initial HTML response: markup injected after page load is only seen by consumers that execute JavaScript, which excludes most AI crawlers.
- Should I use one script block or several?
- Either works. Several separate blocks are simpler to generate from separate components, and a single @graph is easier to keep internally consistent because every entity in it can reference the others by @id. The one thing to avoid is duplicating the same entity across blocks with no identifier tying them together.
- Is microdata or RDFa still worth using?
- Not for new work. Both are supported and both entangle your data with your markup, so a template change silently breaks the structured data. JSON-LD keeps the two separate, which is why Google recommends it and why it is the only one of the three that is pleasant to maintain.