Skip to content
JSON-LDschema.orgTechnical

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 Updated 7 min read 1,237 words This page in Markdown

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

TypeWhat it doesProperties not to miss
OrganizationIdentify the entityname, url, logo, sameAs, contactPoint
LocalBusinessA business with a physical addressaddress, geo, openingHoursSpecification, telephone, areaServed, priceRange
Service / ProductWhat you sellname, description, provider, offers, areaServed
OfferPrice and its validityprice, priceCurrency, availability, validThrough
FAQPageReal questions and answersmainEntity, acceptedAnswer
Article / BlogPostingEditorial contentheadline, datePublished, dateModified, author
BreadcrumbListPosition within the siteitemListElement, 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.

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 checks conformance to the vocabulary: types, properties, structure. The Rich Results Test 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.

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.

One URL is enough. The audit returns the score, the twelve checks and the detail of every failure. The first file generation is on us.

Audit my site for free

Back to the blog

Free audit

See what AI actually reads about you.

One URL is enough. The audit returns the score, the twelve checks and the detail of every failure. The first file generation is on us.

Custom project or enterprise

By phone

REPLY WITHIN 48H · ENGLISH & FRENCH

© 2026 Indexonic. All rights reserved.

Legal notice The Web Master — Marrakech