WriteMySEO / Blog / Modeling Your Site as One Schema.org Graph With @id
SEO technology

Modeling Your Site as One Schema.org Graph With @id

How to use @id references to link Organization, WebSite, WebPage, and Article nodes into a coherent graph — plus where Google follows references and where it won't.

Most sites emit structured data as a pile of disconnected assertions. The Article block says the publisher is "Acme". The Organization block, injected by a different plugin, also says "Acme". The BreadcrumbList sits in its own script tag. Nothing indicates that these describe the same entities, so a consumer parsing the page sees three or four unrelated things that happen to share a name string.

JSON-LD was designed to avoid exactly this. @id gives every node a stable identifier, and any parser doing standard JSON-LD processing merges nodes that share one. Used deliberately, it turns your markup from a set of isolated rich-result payloads into a single description of your site: one Organization, one WebSite, one WebPage per URL, each Article attached to the page it lives on.

Why disconnected blocks are a real problem, not a tidiness issue

A JSON-LD node without an @id is a blank node. It exists only inside that document, and it can never be referenced. Two blank nodes with identical properties are still two nodes.

That has practical consequences:

Giving nodes identifiers makes conflicts collapse into one place, where they're either consistent or obviously broken.

What @id actually is, and how to choose values

@id is the node's identifier — an IRI. url is a property describing where the thing lives on the web. They are not the same field, and conflating them causes collisions: on a blog post, both the WebPage and the Article legitimately have url equal to the post URL, but they are different nodes and need different identifiers.

The convention that solves this is absolute URLs with fragment identifiers:

Node@id patternDefined inLinks out to
Organizationhttps://example.com/#organizationSitewide layerlogo, sameAs
WebSitehttps://example.com/#websiteSitewide layerpublisher → Organization
WebPage{pageUrl}#webpagePage layerisPartOf → WebSite, breadcrumb
Article{pageUrl}#articleTemplate layerisPartOf/mainEntityOfPage → WebPage, author, publisher
Person (author)https://example.com/authors/jane/#personAuthor layersameAs, worksFor
BreadcrumbList{pageUrl}#breadcrumbPage layeritemListElement
ImageObject{pageUrl}#primaryimagePage layer

An @id does not have to resolve to a document. It has to be stable, globally unique, and identical everywhere the entity is mentioned. That last point is where implementations fail: trailing-slash inconsistency, http versus https, or www drift will silently split one entity into several.

What the assembled graph looks like

One script tag, one @graph, references instead of repetition:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Acme",
      "url": "https://example.com/",
      "logo": {"@type": "ImageObject", "@id": "https://example.com/#logo",
               "url": "https://example.com/logo.png", "width": 600, "height": 60},
      "sameAs": ["https://www.wikidata.org/wiki/Q000000",
                 "https://www.linkedin.com/company/acme/"]
    },
    {
      "@type": "WebSite",
      "@id": "https://example.com/#website",
      "url": "https://example.com/",
      "name": "Acme",
      "publisher": {"@id": "https://example.com/#organization"}
    },
    {
      "@type": "WebPage",
      "@id": "https://example.com/blog/crawl-budget/#webpage",
      "url": "https://example.com/blog/crawl-budget/",
      "name": "How crawl budget works",
      "isPartOf": {"@id": "https://example.com/#website"},
      "breadcrumb": {"@id": "https://example.com/blog/crawl-budget/#breadcrumb"},
      "primaryImageOfPage": {"@id": "https://example.com/blog/crawl-budget/#primaryimage"}
    },
    {
      "@type": "Article",
      "@id": "https://example.com/blog/crawl-budget/#article",
      "headline": "How crawl budget works",
      "datePublished": "2024-03-11T09:00:00-05:00",
      "dateModified": "2025-01-08T14:20:00-05:00",
      "mainEntityOfPage": {"@id": "https://example.com/blog/crawl-budget/#webpage"},
      "author": {"@type": "Person", "@id": "https://example.com/authors/jane/#person",
                 "name": "Jane Reyes", "url": "https://example.com/authors/jane/"},
      "publisher": {"@id": "https://example.com/#organization"},
      "image": {"@id": "https://example.com/blog/crawl-budget/#primaryimage"}
    }
  ]
}

Note the pattern for author: the node is defined and identified inline. That is deliberate, and the next section explains why.

Where Google follows references, and where you should not rely on it

Google documents JSON-LD as its recommended format and states that a page can carry multiple structured data blocks. Its parsers handle @graph — you can confirm this yourself with the Rich Results Test, which resolves nodes inside a graph and reports eligibility. In practice, @id references within the same page generally resolve.

What Google has not documented is a guarantee that every rich-result feature will chase an @id reference to find a required property. Feature documentation lists required properties for an item; it does not promise reference resolution as a way of satisfying them. And nothing suggests Google fetches a different URL to resolve an @id — an author node identified as /authors/jane/#person with no properties on the current page is, for eligibility purposes, empty.

So the decision rule:

  1. Inline the properties a rich result requires, in the node that needs them. author.name, headline, image, offers, aggregateRating — define them where they're used.
  2. Use @id for identity and de-duplication, not as a storage-saving mechanism. Referencing your Organization node by @id from publisher is safe because the Organization node is fully defined on the same page.
  3. Repeat the sitewide nodes on every page. Same @id, same values. Consumers merge; you lose nothing and you stop depending on cross-page resolution.

This costs a few hundred bytes per page and removes the main failure mode.

Assembling the graph in a real codebase

The architectural mistake is letting each component emit its own script tag. Instead, have templates contribute nodes to a collector that serializes once:

Derive every @id from the canonical URL, normalized once — same scheme, host, and trailing-slash policy your canonical tags use. If pagination, parameters, or locale variants produce different URLs, they produce different WebPage nodes, which is correct. For international sites, keep a single Organization @id on the root domain rather than one per locale, unless the locales genuinely represent separate legal entities.

Validating that the graph is actually connected

The Rich Results Test answers "am I eligible?" It does not answer "is my graph coherent?" Use the Schema.org Validator for the second question — it shows the full node structure, including types Google ignores.

The cheap automated check is a dangling-reference test: any @id that is referenced but never defined on the page. With the JSON-LD extracted to graph.json:

jq -r '
  [..|objects|select(has("@id"))|.["@id"]] as $refs |
  [.["@graph"][]|.["@id"]] as $defined |
  ($refs - $defined) | unique[]
' graph.json

Anything printed is either a legitimate external identifier (a Wikidata URL in sameAs) or a broken internal reference. Wire that into the same pre-deploy check that guards your other structured data.

What to do this week

  1. Fetch three representative URLs — homepage, a category, an article — and extract all JSON-LD. Count how many distinct nodes describe your organization. If it's more than one per page, you have a merge problem.
  2. Pick and document your @id conventions, derived from canonical URLs. Write them down before implementing; inconsistency is the failure mode.
  3. Consolidate emission into a single @graph per page, contributed to by layered templates rather than independent plugins.
  4. Keep required rich-result properties inline. Use references for identity, not for compression.
  5. Add the dangling-reference check to CI, and re-run the Schema.org Validator after any template change that touches URLs.
structured dataschema.orgjson-ld

WriteMySEO produces marketing content, not legal, medical, financial, or compliance advice. Figures cited reflect publicly reported industry data at time of writing and shift over time.

Get started

We write this well about your industry, every month.

AI-drafted, human-reviewed SEO content on a flat subscription. Blog posts, metadata, schema, and internal links, shipped on a monthly rhythm.

See plans

More from the blog