@graph and JSON-LD Nesting: Connecting Your Entities Into One Knowledge Object
- February 7, 2019
- Technical SEO
Most schema markup fails to build an entity graph not because the types are wrong, but because each block sits in isolation. Search engines see an Organization here, an Article there, and a vague Person somewhere else, with nothing telling them these describe the same connected reality. The fix is structural: wrap everything in a single @graph and wire your nodes together with @id references so Google reads one knowledge object instead of several disconnected fragments.
What the @graph container actually does
A standard JSON-LD block describes one root entity. The moment you need to describe two or three peer entities on the same page (your company, the author, and the article they wrote), you hit a wall: you either nest everything inside one type awkwardly, or you ship multiple separate <script> tags that have no idea the others exist.
The @graph key solves this. It holds an array of nodes that all share one @context, signaling that they belong to the same document-level set of facts. Structurally it looks like this:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "https://example.com/#org" },
{ "@type": "WebSite", "@id": "https://example.com/#website" },
{ "@type": "Person", "@id": "https://example.com/#alex" },
{ "@type": "Article", "@id": "https://example.com/post/#article" }
]
}Each node is a flat, top-level object. None is buried inside another. They become a connected set through the @id references you add next, not through physical nesting.
@id: the glue that turns nodes into a graph
An @id is a stable, globally unique URI that names an entity. It is not a link a user clicks and not necessarily a real page; it is an identifier. Once a node declares its @id, any other node can point at that exact entity by reference instead of redefining it.
The pattern has two halves:
- Declare once. The Organization node carries its full definition plus
"@id": "https://example.com/#organization". - Reference everywhere. The Article's
publisherbecomes{ "@id": "https://example.com/#organization" }, a single-key object that says "the publisher is that entity I already defined."
This is the difference between describing the same company three times (and risking three slightly different versions) and describing it once as a canonical node that the WebSite, the Article, and the author's worksFor all resolve to. Conflicting duplicate definitions are one of the most common reasons rich-result eligibility quietly degrades.
A naming convention for @id values
Consistency matters more than cleverness. Pick a scheme and apply it site-wide so the same entity always carries the same identifier on every page.
- Site-wide singletons live on the homepage origin with a fragment:
https://example.com/#organization,https://example.com/#website,https://example.com/#logo. - Page-scoped entities hang off the page URL:
https://example.com/blog/post-slug/#article,.../#webpage,.../#breadcrumb. - People get one durable identifier reused across every article they touch:
https://example.com/author/alex/#person.
The fragment (#organization) keeps the identifier distinct from any real URL while staying readable. Never let the same logical entity drift to a different @id between templates, that breaks the connection silently.
Building the connected node set
Here is a working skeleton tying an Organization, a Person, and an Article into one object. Note that every relationship is a reference, not a re-declaration.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://example.com/#organization",
"name": "Example Co",
"url": "https://example.com/",
"logo": {
"@type": "ImageObject",
"@id": "https://example.com/#logo",
"url": "https://example.com/logo.png"
}
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"url": "https://example.com/",
"publisher": { "@id": "https://example.com/#organization" }
},
{
"@type": "Person",
"@id": "https://example.com/author/alex/#person",
"name": "Alex Rivera",
"url": "https://example.com/author/alex/",
"worksFor": { "@id": "https://example.com/#organization" },
"sameAs": [
"https://www.linkedin.com/in/alexrivera",
"https://twitter.com/alexrivera"
]
},
{
"@type": "Article",
"@id": "https://example.com/blog/json-ld-graph/#article",
"headline": "Modeling Entities With @graph",
"author": { "@id": "https://example.com/author/alex/#person" },
"publisher": { "@id": "https://example.com/#organization" },
"isPartOf": { "@id": "https://example.com/#website" },
"image": { "@id": "https://example.com/#logo" }
}
]
}Read the relationships out loud and the model is obvious: the Article was written by Alex, who works for Example Co, which also publishes the WebSite this Article is part of. One coherent story, zero repeated definitions.
Why this beats stacked separate blocks
- Entity consolidation. Google's parser merges nodes that share an
@id. A single canonical Organization node strengthens your entity rather than splitting authority across near-duplicates. - Smaller, DRY markup. You define the publisher and logo once and reference them, instead of copying full objects into every Article.
- Cleaner author-to-publisher links.
worksForandauthorresolving to real declared nodes give search engines explicit, verifiable relationships, exactly the kind of connective tissue that supports E-E-A-T signals. - Reusability across pages. The same Person
@idappearing on twenty articles tells Google one author produced all of them.
Common mistakes
- Referencing an
@idthat no node defines. Ifauthorpoints to#personbut no Person node with that@idexists in the graph, the reference dangles and the relationship is lost. Every referenced@idmust be declared somewhere, ideally in the same graph. - Reusing one
@idfor two different types. An@idnames a single entity. Don't attach the same identifier to both a WebPage and an Article. - Mismatched identifiers between templates. Your homepage saying
#organizationand inner pages saying#orgcreates two entities Google never merges. Centralize the value. - Nesting full objects when you meant to reference. Embedding a complete Organization object inside every Article re-declares it each time. Use a one-key
{ "@id": "..." }reference instead. - Trailing-slash drift.
example.com/#organizationandexample.com#organizationare different strings. Match your canonical URL format exactly, every time.
How to validate it
Run the page through Google's Rich Results Test and Schema.org's validator. The Rich Results Test confirms eligibility, but for graph integrity the more useful check is whether every reference resolves. Open the parsed output and confirm each { "@id": ... } reference matches a fully defined node. If a referenced entity shows up as a bare URI with no type or properties, you have a dangling reference to fix.
Build the graph once per template, reference your singletons, keep identifiers identical site-wide, and you give search engines a single, unambiguous knowledge object, the connected entity model they are actually trying to reconstruct anyway.
Want this handled properly on your site?
It is exactly the kind of work an advanced technical SEO audit covers. See how an advanced SEO audit works →
Claude Vincent is a technical SEO consultant focused on crawlability, rendering, and AI-search visibility. He writes the field guides and case studies at SEO ProCheck, with a bias toward the durable, unglamorous work that decides whether search engines and AI answer engines can actually read and cite a site.
About SEO ProCheck
Technical SEO consulting and GEO strategy with 20 years of enterprise experience. Case studies, resources, and tools for search and AI visibility.
Work With Me
Technical SEO audits, GEO strategy, site migrations, and international SEO. Hourly consulting for teams who need hands-on support, not just reports.








