The pattern almost everyone starts with You add an Organization block to the footer. An Article block to the article template. A BreadcrumbList to the breadcrumb component. Three separate blocks, each describing one thing. It works — Google reads all of them — and it's where most sites stop. The cost isn't visible immediately. It shows up the first time you rename the company, change the logo URL, or move domains. Now you have three places containing the same facts, and nothing enforces that they agree. One gets updated, the others don't, and your structured data 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 — a stable identifier. A URL is conventional. Once a node has an @id, any other node can refer to it instead of repeating its properties. So instead of the Article containing its own copy of the publisher's name and logo: { "@type": "Article", "publisher": { "@type": "Organization", "name": "Acme", "logo": { "@type": "ImageObject", "url": "https://acme.com/logo.png" } } } Enter fullscreen mode Exit fullscreen mode …it contains a reference: { "@type": "Article", "publisher": { "@id": "https://acme.com/#organization" } } Enter fullscreen mode Exit fullscreen mode The full description lives once, in a node other nodes point at. Change the logo in that one place and every reference is correct. The shape that works One @graph array in the page head, containing in order: Organization WebSite WebPage (for the current URL) whatever page-specific nodes apply — Article, FAQPage, HowTo, Product The Organization and WebSite nodes are identical on every page — that's the point. They're declared once per page but defined consistently, and page-specific nodes reference them by @id rather than restating them. { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://acme.com/#organization", "name": "Acme", "url": "https://acme.com/" }, { "@type": "WebSite", "@id": "https://acme.com/#website", "url": "https://acme.com/", "publisher": { "@id": "https://acme.com/#organization" } }, { "@type": "WebPage", "@id": "https://acme.com/blog/post#webpage", "url": "https://acme.com/blog/post", "isPartOf": { "@id": "https://acme.com/#website" }, "datePublished": "2026-03-14", "dateModified": "2026-08-02" }, { "@type": "Article", "@id": "https://acme.com/blog/post#article", "headline": "…", "mainEntityOfPage": { "@id": "https://acme.com/blog/post#webpage" }, "publisher": { "@id": "https://acme.com/#organization" } } ] } Enter fullscreen mode Exit fullscreen mode Four nodes, one block, every relationship explicit. Three ways this goes wrong 1. Referencing an @id that doesn't 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 isn't an error, it's silently missing data. Always include the node you reference. 2. Reusing the same @id for different things. Identifiers must be unique. Two nodes sharing an @id means you've declared two contradictory definitions of one entity, and the outcome depends on which one a consumer reads first. 3. 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 cross-page relationship you were expressing breaks. Dates deserve their own section 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 doesn't — and the pattern is trivially detectable: a modified date that advances daily while the words stay identical tells any consumer the field is noise, which means it gets discounted on the pages where it was actually true. If you use a build system, wire dateModified to a per-page content hash, not the build timestamp: import crypto from "node:crypto"; const hash = crypto .createHash("sha256") .update(pageBody) .digest("hex") .slice(0, 12); // store hash → date in a committed JSON file // update the date only when the hash changes Enter fullscreen mode Exit fullscreen mode The field then 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 — a dangling reference is not a syntax error. The check that catches it takes a minute and is manual: Extract every @id defined in the page Extract every value used in a reference Confirm every reference appears in the defined list On a page with six nodes this is faster by eye than to automate. I built a small generator for this shape that validates required properties as you type — SerpPrism's JSON-LD generator. It's client-side, nothing is uploaded. There's also a longer version of this post with a few more edge cases on the site itself. Happy to hear how other people handle the dateModified problem — it's the one I've seen go wrong most often in production.
Your JSON-LD is duplicated in three places. Here's the fix.
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.