What Is JSON-LD Structured Data and How Do You Implement It in 2026?

JSON-LD is the block of machine-readable facts you attach to a page so a crawler does not have to guess them from your layout. By the end of this page you will be able to ship a correct block, connect your entities with a single @graph, and gate the whole thing in CI so a broken @id never reaches production.

CategoryStructured data & machine readability StatusStable Maintained bySchema.org / W3C Glippy checkStructured Data (category 1)

JSON-LD Structured Data (Schema.org in JSON-LD) is a JSON document embedded in a page that states, in a shared vocabulary, what the page is about: the article, the product, the company, the author, and how those things relate to each other. It uses the JSON-LD serialisation of linked data, a W3C Recommendation since 16 July 2020, with terms drawn from the Schema.org vocabulary that Google, Microsoft, Yahoo and Yandex maintain jointly. It exists so a machine can read your facts directly instead of inferring them from headings, class names and paragraph order.

Why JSON-LD Structured Data matters for AI visibility

Everything else a crawler reads from your page is an inference. A heading might be a title or a section label. A date string might be publication, last review, or a comment timestamp. A price might be the current price or the one crossed out next to it. Extraction from prose and layout is probabilistic, and it degrades on templates the extractor has not seen before. A JSON-LD node is not probabilistic: datePublished is the publication date because the vocabulary says so. That is the whole trade. You spend a few kilobytes to remove ambiguity from the facts you most want quoted correctly.

Be clear about what this does not buy you. Google states plainly in its AI features documentation that there is no special schema.org structured data you need to add to appear in AI Overviews or AI Mode, and its generative AI optimisation guide repeats it: structured data is not required for generative AI search. Treat anyone selling you a schema type as an AI ranking lever with suspicion. The payoff is narrower and more durable than that: identity. When every page on your site points at the same @id for your organisation, your authors and your products, a retrieval system reading ten of your URLs sees one company mentioned ten times rather than ten unrelated strings that happen to look similar. That is what the @graph pattern below is for, and it is the part most implementations skip.

Where the spec lives

Two primary sources define the format and the vocabulary, and two validators answer different questions about your output. Read the first two before you argue with the last two.

  • JSON-LD 1.1 (W3C Recommendation) - the syntax itself. Section 3.3 defines node identifiers and section 4.9 defines named graphs, which is where @id and @graph actually come from. Reach for it when a validator disagrees with you about relative IRIs.
  • Schema.org vocabulary - the type and property definitions. Every type page lists the expected value types and the parent type, which is the fastest way to check whether a property is legal on the type you are using.
  • Schema.org release list - the vocabulary is versioned. The current release is 30.0, dated 2026-03-19. Check here before adopting a type you saw in a blog post, because pending terms live under a separate namespace until they are promoted.
  • Schema Markup Validator - vendor-neutral vocabulary checking. Accepts a live URL or a pasted snippet and tells you whether your properties exist and are used on a legal type. This is the tool that catches typos and invented properties.
  • Google Rich Results Test - Google eligibility, which is a different question. Also renders the page first, so it is the quickest way to confirm that JavaScript-injected markup survives.
  • Google structured data feature gallery - the current list of types that still produce a rich result. Worth re-reading periodically: FAQ and HowTo are no longer on it.

Three ways to implement JSON-LD Structured Data

The first example is the smallest block that a crawler can actually use, which is where most sites should start and where many should stop. The second is the connected @graph, for anyone running more than a handful of templates who wants their entities to resolve to one identity across the whole site. The third generates that graph from a single source of truth in your build and fails CI when an @id drifts away from the canonical URL, which is what you need once more than one person can edit a template.

01

The minimum block a crawler can actually use

One page, one entity, no cross-references. Use this when you have a blog or a docs site and you want correct markup shipped this afternoon. Server-render it into the head so it is present in the initial HTML.

htmltemplates/article.html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "@id": "https://example.com/blog/rate-limiting/#article",
  "mainEntityOfPage": "https://example.com/blog/rate-limiting/",
  "headline": "Rate limiting without a shared cache",
  "description": "Keeping per-tenant limits accurate across ten stateless nodes.",
  "image": ["https://example.com/img/rate-limiting-16x9.png"],
  "datePublished": "2026-03-04T09:00:00+01:00",
  "dateModified": "2026-08-11T14:20:00+02:00",
  "inLanguage": "en",
  "author": {
    "@type": "Person",
    "name": "Rosa Hale",
    "url": "https://example.com/team/rosa-hale/"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Example Ltd",
    "url": "https://example.com/",
    "logo": { "@type": "ImageObject", "url": "https://example.com/assets/logo-600x60.png" }
  }
}
</script>

What this does: it answers the four questions an extractor asks about a document, which are what it is, who wrote it, when it was published and who published it, without the extractor having to parse a byline. Google's Article guidance treats every property here as recommended rather than required, so the block is valid even if you drop image, but each one you drop is a fact the machine has to guess again.

02

One connected @graph instead of four loose blocks

Use this once the same organisation, the same author and the same site appear across hundreds of URLs. The @graph array holds several nodes in one document, and @id lets a node reference another by identity rather than by repeating it. Note the absolute URLs: a bare fragment resolves against the current page, which would mint a different organisation on every URL.

htmltemplates/partials/schema-graph.html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Example Ltd",
      "url": "https://example.com/",
      "logo": { "@type": "ImageObject", "url": "https://example.com/assets/logo-600x60.png" },
      "sameAs": ["https://www.linkedin.com/company/example", "https://github.com/example"]
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com/",
      "name": "Example",
      "publisher": { "@id": "https://example.com/#organization" },
      "inLanguage": "en"
    },
    {
      "@type": "Person",
      "@id": "https://example.com/team/rosa-hale/#person",
      "name": "Rosa Hale",
      "url": "https://example.com/team/rosa-hale/",
      "worksFor": { "@id": "https://example.com/#organization" }
    },
    {
      "@type": ["WebPage", "BlogPosting"],
      "@id": "https://example.com/blog/rate-limiting/#webpage",
      "url": "https://example.com/blog/rate-limiting/",
      "headline": "Rate limiting without a shared cache",
      "isPartOf": { "@id": "https://example.com/#website" },
      "author": { "@id": "https://example.com/team/rosa-hale/#person" },
      "publisher": { "@id": "https://example.com/#organization" },
      "datePublished": "2026-03-04T09:00:00+01:00",
      "dateModified": "2026-08-11T14:20:00+02:00"
    }
  ]
}
</script>

What this does: the author node lives at the author's own page URL plus a fragment, so the article, the author bio page and the team listing can all point at the same string and be understood as one person rather than three. The page node is dual-typed so a single node answers both "what URL is this" and "what document is this", and the sameAs array on the organisation gives a resolver somewhere external to reconcile the entity against.

03

Generate the graph at build time and fail CI on a broken @id

Hand-maintained graphs rot. Someone adds a trailing slash to a route, someone else deletes an author page, and you are left with references pointing at nodes that no longer exist. Build the graph from one module and assert two things during the build: that a page node exists for the canonical the template actually rendered, and that no internal @id reference is dangling.

javascriptlib/schema-graph.js
// One source of truth for every JSON-LD node the site emits.
const SITE = 'https://example.com';
const ORG = SITE + '/#organization';
const WEBSITE = SITE + '/#website';

export function buildGraph(page) {
  return { '@context': 'https://schema.org', '@graph': [
    { '@type': 'Organization', '@id': ORG, name: 'Example Ltd', url: SITE + '/',
      sameAs: ['https://www.linkedin.com/company/example'] },
    { '@type': 'WebSite', '@id': WEBSITE, url: SITE + '/', name: 'Example',
      publisher: { '@id': ORG }, inLanguage: 'en' },
    { '@type': ['WebPage', 'BlogPosting'], '@id': page.canonical + '#webpage',
      url: page.canonical, headline: page.title, description: page.description,
      isPartOf: { '@id': WEBSITE }, publisher: { '@id': ORG },
      datePublished: page.published, dateModified: page.modified,
      author: { '@id': page.author.url + '#person' } },
    { '@type': 'Person', '@id': page.author.url + '#person', name: page.author.name,
      url: page.author.url, worksFor: { '@id': ORG } },
  ] };
}

// Call this from the build with the canonical the template really rendered.
export function assertGraph(graph, renderedCanonical) {
  const defined = new Set(graph['@graph'].map((n) => n['@id']));
  if (!defined.has(renderedCanonical + '#webpage')) {
    throw new Error('no page node for canonical ' + renderedCanonical);
  }
  for (const raw of JSON.stringify(graph).match(/"@id":"[^"]+"/g) || []) {
    const id = raw.slice(7, -1);
    if (id.startsWith(SITE) && !defined.has(id)) throw new Error('dangling @id: ' + id);
  }
  return graph;
}

What this does: it turns two silent failure modes into build failures. Passing the rendered canonical separately is the point: if the template starts emitting /blog/rate-limiting while the graph still says /blog/rate-limiting/, the assertion throws instead of shipping two identities for one page. Wire assertGraph into the same CI step that builds the pages, then spot-check one built URL per template in the Rich Results Test.

Implementation guidelines

These are the things that break in production rather than in the validator.

  1. Derive every @id from the canonical URL, not from a variable that looks like it. Build the identifier from the same string your canonical link element emits, with a fragment appended. If one carries a trailing slash and the other does not, or one is lower-cased and the other is not, you have quietly created two entities for one page.
  2. Never use a bare fragment as an @id. Writing "@id": "#organization" is legal JSON-LD, but relative identifiers resolve against the document base, so every URL on your site mints its own organisation node. Always write the absolute form.
  3. Keep each page self-contained. Google processes URLs independently and will not fetch an @id from another page to fill in properties that are missing here. Reference by @id for identity, but still put the properties this page needs on this page. Google's own policies go further and recommend repeating the same structured data on duplicate URLs, not only on the canonical.
  4. Audit for duplicate entities after any plugin or CMS change. Three SEO plugins will happily emit three Organization nodes with different identifiers and slightly different names. Search the rendered HTML for how many times your organisation name appears in a JSON-LD node before you assume the graph is clean.
  5. Render it server side. Google can read JSON-LD injected by JavaScript, but that costs a render pass and most other crawlers, including Glippy's default fetch, score the initial HTML. If the markup only exists after hydration, assume a meaningful share of readers never see it.
  6. Validate in two places, because they answer different questions. The Schema Markup Validator tells you whether your vocabulary is correct. The Rich Results Test tells you whether Google will do anything visible with it. A block can pass one and fail the other, and both results are useful.
  7. Do not budget engineering time against a retired rich result. Google removed FAQ rich results on 7 May 2026 and pulled the documentation the following month, after doing the same to HowTo earlier. Both remain valid Schema.org types and still describe your page honestly to any other consumer, so there is no need to rip them out, but do not expect a search feature in return.

Do this, not that

Do

  • Use https://schema.org as the @context and absolute URLs for every @id.
  • Give the organisation one home at the site root and reference that same identifier from every other page.
  • Add sameAs to the organisation and to authors, pointing only at profiles you actually control.
  • Format dates as ISO 8601 with an offset, for example 2026-03-04T09:00:00+01:00.

Do not

  • Do not let a URL change silently change an @id: a trailing slash or a tracking parameter creates a second entity.
  • Do not mark up an author, rating, price or date that does not appear in the visible page content.
  • Do not reuse one @id for two different things, which merges them into a single confused node.
  • Do not touch dateModified on a nightly rebuild when the body copy has not changed.

How Glippy checks this

Structured Data and Schema is category 1 in Glippy, weighted 1.5, and it is scored out of 100 across seven signals: JSON-LD present and parseable (25), at least one GEO-critical type such as Article, Organization, Product, WebPage or Person (25), required and recommended property completeness for the types it finds (up to 20), speakable (10), sameAs (10), plus small credits for microdata and RDFa. A block that is present but fails to parse scores zero and is reported separately, so a stray trailing comma costs you the full 25 rather than degrading gracefully. Glippy expands @graph into individual nodes before scoring, so the connected pattern is not penalised: running the two examples above through the engine, the single Article block scores 65 and the four-node graph scores 80, the difference coming from the extra recognised types and from sameAs being visible at the top level. The Structured Data Checker shows the per-signal breakdown for any URL.

Check your JSON-LD Structured Data setup

Glippy runs 240+ checks across 16 categories on any page, including Structured Data (category 1). No sign-up required.

Frequently asked questions

Google supports the application/ld+json script tag in either the head or the body, so placement is a matter of convention rather than correctness. The head is the usual choice because it keeps the markup away from editable content and makes it easier to spot in a view-source check. What matters far more is that the block appears in the initial server response: markup that only exists after client-side hydration is invisible to crawlers that do not run JavaScript.

No. Multiple blocks on one page are legal and are merged by consumers, but they cannot reference each other cleanly and they make duplicate entities easy to create by accident. A single document with a @graph array holding every node is the tidier pattern: one place to look, one set of identifiers, and cross-references by @id instead of repeated nested objects. Both approaches validate, so this is a maintainability decision rather than a compliance one.

Yes, but for a different reason than in 2019. A retired rich result means the visual treatment in search results ended, not that the markup became invalid, and around two dozen feature types in Google's gallery still produce one. The broader case is machine readability: Google is explicit that no special schema is required for its AI features, yet structured data remains the only part of your page that states your facts unambiguously to any consumer that reads it, including validators, feed importers and agent tooling.

They do different jobs even when they contain similar strings. @id is a JSON-LD keyword that names the node inside the graph, so two nodes sharing an @id are the same thing and a reference elsewhere in the document resolves to it. url is an ordinary Schema.org property describing where the thing lives on the web. The usual convention is to build @id from the canonical URL plus a fragment such as #organization or #person, which keeps identifiers unique and readable while leaving url free to hold the plain address.

Reviewed against the primary sources on . These standards move quickly, so check the linked specs before you ship.

Check your site for AI search readiness

Start free with the Glippy Chrome extension for instant page checks. Scaling up? Automate audits across many URLs and your whole sitemap with the Glippy MCP server.

Add Glippy to Chrome – free Automate with the MCP server →