# JSON-LD structured data: the pattern that makes your facts citable

> The prose on your page invites interpretation. JSON-LD does not: it is the one place where your facts are typed, linked and unambiguous. Here is how to write it so a model uses it — and the traps that void it.

Published: 2026-08-20 · Updated: 2026-08-26 · 7 min read
Source: https://indexonic.com/en/blog/json-ld-structured-data-for-answer-engines/

An answer engine reads your page like a hurried human: it extracts meaning, with a margin of error. It reads your JSON-LD like a database: with none. That is the whole value of structured data in the assistant era — not a ranking bonus, but a way to assert your facts in a form that cannot be misread.

## One graph, stable identifiers

The most common failure is not missing JSON-LD: it is having three blocks — one from the theme, one from an SEO plugin, one from a reviews widget — that contradict each other. An engine that meets two different company names on the same page does not pick one: it becomes cautious.

The pattern that fixes this is the graph. A single `<script type="application/ld+json">` block, a `@graph` key, and inside it every node carries a stable `@id` — a URL with a fragment — that other nodes reference instead of duplicating the information.

*The skeleton: four nodes, wired by @id*

```
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "Organization",
      "@id": "https://example.ma/#organization",
      "name": "Atlas Cargo SARL",
      "url": "https://example.ma/" },

    { "@type": "WebSite",
      "@id": "https://example.ma/#website",
      "url": "https://example.ma/",
      "publisher": { "@id": "https://example.ma/#organization" },
      "inLanguage": "en" },

    { "@type": "WebPage",
      "@id": "https://example.ma/pricing#webpage",
      "url": "https://example.ma/pricing",
      "isPartOf": { "@id": "https://example.ma/#website" },
      "about": { "@id": "https://example.ma/#organization" } },

    { "@type": "BreadcrumbList",
      "@id": "https://example.ma/pricing#breadcrumb",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.ma/" },
        { "@type": "ListItem", "position": 2, "name": "Pricing" }
      ] }
  ]
}
</script>
```

Three properties of this pattern deserve understanding rather than copying. The `@id` values must be **stable over time**: that is what lets the same entity be recognised across pages and across visits. References replace duplication: the company name is written once, so it cannot drift. And the last `ListItem` in a breadcrumb has no `item`: the current page does not link to itself.

## The types that actually matter

| Type | What it does | Properties not to miss |
| --- | --- | --- |
| Organization | Identify the entity | name, url, logo, sameAs, contactPoint |
| LocalBusiness | A business with a physical address | address, geo, openingHoursSpecification, telephone, areaServed, priceRange |
| Service / Product | What you sell | name, description, provider, offers, areaServed |
| Offer | Price and its validity | price, priceCurrency, availability, validThrough |
| FAQPage | Real questions and answers | mainEntity, acceptedAnswer |
| Article / BlogPosting | Editorial content | headline, datePublished, dateModified, author |
| BreadcrumbList | Position within the site | itemListElement, position |

A note on `FAQPage`. Google heavily restricted the matching rich result, and it no longer displays for most sites. The markup still earns its place here for a different reason: it presents your questions and answers pre-segmented, in exactly the shape an answer engine reuses. The rich result went away; the extractability did not.

## The consistency rule — the one nearly everyone breaks

Markup must describe what the page shows. A price displayed as 4,500 MAD and an `Offer` saying 3,900; an address in Casablanca in the footer and in Rabat in the JSON-LD; opening hours matching no visible text — each of those turns an asset into a negative signal. Google withholds rich results for markup that is not representative of the content, and a model that spots a contradiction simply stops treating you as a reliable source.

> **The worst case, and it was ours** — Our own generator once produced a `sameAs` pointing at a Wikidata identifier inferred from the brand name — which did not exist and answered 404. The file looked excellent and asserted a checkable falsehood. We removed the feature and imposed the rule that followed: no URL is published until it has been fetched and answered 200.

Apply the same rule to yourself. `sameAs` should hold only profiles you genuinely control and that actually answer: your LinkedIn page, your business listing, your Instagram account. An invented or abandoned profile is worse than an empty array.

## A complete example: a Moroccan local business

*LocalBusiness — adapt it, invent nothing*

```
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "@id": "https://example.ma/#business",
  "name": "Atlas Cargo SARL",
  "description": "Freight and customs clearance between Casablanca, Tanger Med and Europe.",
  "url": "https://example.ma/",
  "telephone": "+212522000000",
  "email": "contact@example.ma",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "45 boulevard Zerktouni",
    "addressLocality": "Casablanca",
    "postalCode": "20250",
    "addressCountry": "MA"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": 33.589886, "longitude": -7.633076 },
  "areaServed": [
    { "@type": "City", "name": "Casablanca" },
    { "@type": "City", "name": "Tangier" },
    { "@type": "Country", "name": "Morocco" }
  ],
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "08:30", "closes": "18:00" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "09:00", "closes": "13:00" }
  ],
  "availableLanguage": ["ar", "fr", "en"],
  "sameAs": ["https://www.linkedin.com/company/example-ma"]
}
```

Four things make the difference in a Moroccan context. The phone number is written in international format with no spaces: that is what makes it comparable across sources. `addressCountry` takes the code `MA`, not the word "Morocco". `areaServed` is worth spelling out whenever you work beyond your own city — it is the property that answers "who does this in Tangier?". And `availableLanguage` genuinely matters here: the question may arrive in Arabic, French or English, and you have just said you answer in all three.

## The WordPress case: three sources, one truth

On a typical WordPress site the markup comes from three places at once: the theme emits some, the SEO plugin emits more, and a reviews or booking add-on contributes its own. Nobody decided this, and nobody sees it — until the audit. The procedure is always the same: view the page source, search every occurrence of `application/ld+json`, and count.

*How many blocks does your page really publish?*

```
curl -sS https://your-domain.com/ | grep -c 'application/ld+json'
# More than 1: work out who emits what, keep one source, switch the others off.
```

Then choose the source that stays — usually the SEO plugin, because it survives a theme change — and disable the others in their settings. If an add-on refuses to go quiet, a filter in the child theme is still better than three contradictory graphs. The goal is not the richest markup: it is one piece of markup that is correct.

## What voids the whole block

- **Invalid JSON.** One trailing comma, one curly quote pasted from a word processor, and the entire block is ignored — silently.
- **Invented ratings.** An `aggregateRating` with no real reviews visible on the page is a direct policy violation, and the easiest infraction to detect automatically.
- **Images that answer 404.** A dead `logo` or `image` disqualifies the node carrying it.
- **Unstable `@id` values.** If they change on every deploy, the entity is recreated at every visit and never accumulates anything.
- **Competing blocks.** Theme plus SEO plugin plus reviews widget: three sources, three truths. Consolidate into one graph.
- **Markup for content that is not on the page.** A JSON-LD FAQ whose questions appear nowhere visually is non-representative markup.

## Validate, then verify

Two free tools, two different jobs. [validator.schema.org](https://validator.schema.org/) checks conformance to the vocabulary: types, properties, structure. [The Rich Results Test](https://search.google.com/test/rich-results) answers a different question — whether Google can render a special display from it. Perfectly valid markup may qualify for no rich result at all; that is not an error, the two goals are separate.

Neither tool checks what matters most: that your claims are true and consistent with the page. That check stays human — or automated with an explicit rule, like ours: every published URL is fetched before publication, and anything other than a 200 blocks the file from shipping.

> **Free check** — Indexonic's audit extracts your graph, validates it, tests every URL inside it and flags contradictions between the markup and the visible text.


## Frequently asked questions

### JSON-LD, microdata or RDFa?
JSON-LD. It is Google's recommended format, it lives in a block separate from the HTML so it survives a redesign, and it can be read back without untangling markup from layout.

### Is FAQPage markup still worth anything?
For rich results, barely: Google has heavily restricted the display. For answer engines, yes: it hands over your questions and answers pre-segmented, in the exact shape an assistant reuses.

### Does every page need markup?
Every page should carry at least a WebPage node wired to the WebSite and the Organization. Beyond that, mark up what the page actually contains: a service on the service page, an article on an article. Markup without matching content is a fault, not a bonus.

### Where should the block sit in the page?
In the , as a single block. Position has no technical effect, but one central block is what stops competing graphs from reappearing.

---
Indexonic — https://indexonic.com · https://agent.indexonic.com
