skip to content

A product page needs schema.org markup for the Product, for the site's Organization, and for a BreadcrumbList. How do you express several entities on one page in JSON-LD without repeating the Organization inside every node?

level: middleimportance: should knowfreq 36%

answer

  1. one page, many entities
  2. @graph holds the node list
  3. identity, not the DOM id
  4. a stub node with only @id
  5. stable identifiers from shared code

basics

~20 s

Give each entity a stable @id (conventionally a URL with a fragment), define it once, and reference it elsewhere as {"@id": "..."}. Put the nodes in one script block's @graph array, in a top-level array, or in separate script blocks — all three are valid.

solid answer

~50 s

Three shapes are all legal: several `application/ld+json` blocks, one block whose top level is a JSON array, or one block with a `@graph` array — the last is the usual choice because it keeps the page's whole description in one artifact. The key move is identity: give each node a stable `@id`, conventionally the page URL plus a fragment like `#product`, `#organization`, `#breadcrumb`. Define the Organization once with its `@id`, then anywhere it is needed — the Product's `brand`, an Article's `publisher`, an Offer's `seller` — write a stub node `{ "@id": "https://example.com/#organization" }` instead of inlining the whole thing. Consumers merge nodes that share an `@id` into one entity. That keeps the block small, keeps one description of the company, and lets a site-wide layout emit the Organization node while a page template emits the Product.

code

json · 25 lines
json
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Example Labs",
      "url": "https://example.com/",
      "logo": "https://example.com/logo.png"
    },
    {
      "@type": "Product",
      "@id": "https://example.com/mug#product",
      "name": "Ceramic Mug",
      "brand": { "@id": "https://example.com/#organization" },
      "offers": {
        "@type": "Offer",
        "price": "14.00",
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock",
        "seller": { "@id": "https://example.com/#organization" }
      }
    }
  ]
}

go deeper

for a junior

Know that one page can describe several entities, that they can go in one @graph array or in several script blocks, and that @id names an entity.

for a middle

Explain node references: define the Organization once with an @id, then use a stub object containing only that @id wherever it is needed, and note that @context declared at the top covers every node in @graph.

for a senior

Talk about generating identifiers from shared helpers so templates cannot disagree, and about which block emits which node when layout and page render independently.

for a principal

Frame the graph as a site-wide schema you own: which entities are canonical, who emits them, and how you prevent two teams from minting two identities for the same company.

## The problem A real page is not one entity. A product page is a `Product`, plus the `Offer` that sells it, plus the `Organization` that sells it, plus the `BreadcrumbList` that says where it sits in the catalogue. Write that naively and the company's name, logo and URL end up copied into three places in the same block — and into every page on the site. ## Three legal containers **Separate script blocks.** Nothing limits a document to one `application/ld+json` element. A breadcrumb component emits its own block, the product template emits another. This composes beautifully with a component-based UI because no component needs to know what the others emit. **A top-level array.** One block whose contents are `[ {...}, {...} ]`. Perfectly valid JSON-LD. **A `@graph` array.** One object with `@context` at the top and a `@graph` property holding the list of nodes: ```html <script type="application/ld+json"> { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://example.com/#organization", "name": "Example Labs", "url": "https://example.com/" }, { "@type": "Product", "@id": "https://example.com/mug#product", "name": "Ceramic Mug", "brand": { "@id": "https://example.com/#organization" }, "offers": { "@type": "Offer", "price": "14.00", "priceCurrency": "EUR", "availability": "https://schema.org/InStock", "seller": { "@id": "https://example.com/#organization" } } }, { "@type": "BreadcrumbList", "@id": "https://example.com/mug#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Kitchen", "item": "https://example.com/kitchen" }, { "@type": "ListItem", "position": 2, "name": "Ceramic Mug" } ] } ] } </script> ``` `@context` is declared once and applies to every node in the graph. That alone is why `@graph` is the common house style. ## What `@id` is for `@id` is a node's identifier. It is not the DOM `id` attribute, and it is not a directive to a search engine about which URL to index — it is a name for *this thing* so that two mentions can be recognised as the same thing. Because it is an identifier, the convention is to use an absolute URL with a fragment: `https://example.com/#organization` for the company, `https://example.com/mug#product` for the product. Fragments keep the identifiers distinct from the page URLs themselves and make them readable when you are debugging a validator report. Once a node has an `@id`, a **node reference** — an object containing nothing but `@id` — stands in for it anywhere: ```json "publisher": { "@id": "https://example.com/#organization" } ``` A consumer that merges the graph resolves that stub to the full Organization node. If two nodes in the same graph share an `@id`, their properties are merged into one entity, which is how a site-wide layout can contribute `logo` and `sameAs` while a page template contributes nothing but the reference. ## Practical rules that keep this maintainable **Identifiers must be stable.** The whole mechanism rests on the same string appearing in both places. Generate them from one helper (`orgId()`, `productId(sku)`), never by hand in each template. **One block or many is an architecture choice, not a correctness one.** If your framework renders layout and page independently, separate blocks are simpler: the layout emits the Organization, the page emits the Product with a reference to it. If you generate the whole document in one pass, `@graph` gives you one artifact to validate. **Referencing across blocks works too**, because consumers combine all the structured data found on the page — but it is harder to reason about, and a validator report shows you two fragments instead of one graph. Prefer keeping tightly coupled nodes together. **Do not over-model.** `@id` earns its keep when an entity appears in more than one place. A single `Person` author on a single article does not need one; inline it and move on. ## The BreadcrumbList detail worth knowing `BreadcrumbList` takes `itemListElement`, an array of `ListItem` nodes with `position` (1-based), `name`, and `item` (the URL of that step). The final item is the current page; supplying its URL is optional because it is where the user already is. Its ordering is carried by `position`, not by array order alone — filling the positions in is not optional. ## Interview framing This question separates "I pasted a generator's output" from "I designed the site's structured data". The strong answer names `@graph`, names `@id`, and explains node references as deduplication — then adds the operational point that the identifiers must come from shared code, because a typo in one template silently splits one company into two entities.

  • Is @id the same as the element's id attribute or the page's canonical URL?
    No. `@id` is an identifier for a node in the data graph and has no relationship to any DOM `id`. It is conventionally written as a URL because URLs are globally unique, but it makes no claim about which URL should be indexed — it only lets two mentions of an entity be recognised as one.
  • What happens if two nodes in the same graph accidentally share an @id?
    Consumers treat them as one entity and merge their properties, which is the intended behaviour for deliberate references but a bug when it is accidental — a Product and an Article sharing `#main` produce one confused node with both sets of properties. Derive identifiers from shared helpers so collisions cannot happen by copy-paste.
  • Do BreadcrumbList items need anything beyond a name?
    Each `ListItem` needs `position` — a 1-based integer giving its place in the trail — and `name`. `item` carries the URL of that step and is what makes the crumb clickable in a result; it is normally omitted on the final item because that is the current page. Positions must be present and sequential.

saying these in an interview costs you the question

  • Copies the full Organization object into every node
  • Thinks a page may contain only one ld+json block
  • Confuses @id with the DOM id attribute
  • Treats @id as a canonical-URL declaration
  • Omits position on BreadcrumbList ListItem entries

context