The pattern almost everyone starts with
A site adds an Organization block to the footer, an Article block to the article template, and a BreadcrumbList to the breadcrumb component. Three separate <script type="application/ld+json"> blocks, each describing one thing. It works — Google reads all of them — and it is where most sites stop.
The cost is not visible immediately. It shows up the first time you rename your company, change your logo URL, or move to a new domain. Now you have three places that each contain the same facts, and nothing enforces that they agree. One gets updated, the others do not, and the structured data on your site now contradicts itself in a way no error message will tell you about.
What @id actually does
Every node in JSON-LD can carry an @id, which is a stable identifier — a URL is conventional. Once a node has an @id, any other node can refer to it by that identifier instead of repeating its properties.
So instead of the Article block containing its own copy of the publisher's name and logo, it contains "publisher": { "@id": "https://example.com/#organization" }. The full description of that organization lives once, in a node that other nodes point at. Change the logo in that one place and every reference is correct.
The practical payoff is that your structured data becomes a description of relationships rather than a set of isolated assertions. This article was published by that organization, on this website, and sits at this position in this breadcrumb trail. All of it expressed explicitly rather than inferred.
The shape that works
A layout that has held up well across several sites: one @graph array in the page head containing, in order, the Organization, the WebSite, the WebPage for the current URL, and then whatever page-specific nodes apply — an Article, a FAQPage, a HowTo, a Product.
The Organization and WebSite nodes are identical on every page, which is the point: they are declared once per page but defined consistently, and every page-specific node references them by @id rather than restating them. The WebPage node carries datePublished and dateModified, and the page-specific nodes carry their own.
Breadcrumbs attach to the WebPage rather than floating free, because a breadcrumb trail describes a position within a site hierarchy, and the hierarchy belongs to the page.
Where this goes wrong
Referencing an @id that does not exist on the page. If the Article says its publisher is #organization but no node with that @id is present, the reference resolves to nothing. Google handles this by ignoring the property, so the symptom is not an error — it is silently missing data. Always include the node you reference.
Using the same @id for different things. Identifiers must be unique. Two nodes sharing an @id means you have declared two contradictory definitions of one entity, and the outcome depends on which one a consumer reads first.
Absolute versus relative @id values. Use absolute URLs. A relative reference like #organization is interpreted against the page URL, which means the same markup on two domains produces two different identifiers, and any relationship you were trying to express across pages breaks.
Dates deserve their own paragraph
datePublished should never change after the page goes live. dateModified should change when the content genuinely changes, and not otherwise.
Incrementing dateModified on every deploy is a common habit, usually adopted because someone believed freshness helps rankings. It does not, and the pattern is easy to detect: a modified date that advances daily while the words stay identical tells any consumer that the field is noise, which means it will be discounted on the pages where it was actually true.
If you use a build system, wire dateModified to a per-page content hash rather than to the build timestamp. That way the field moves only when the content does.
Checking your markup
Validate with Google's Rich Results Test for eligibility and the Schema Markup Validator for syntax. Neither will tell you that your @id references are dangling, because a dangling reference is not a syntax error.
The check that catches it is manual and takes a minute: extract the list of @id values defined in the page, then extract every value used in a reference, and confirm every reference appears in the defined list. On a page with six nodes this is faster to do by eye than to automate.