Schema @id Problems
The @id keyword gives an entity in JSON-LD a name that other parts of your markup, and other pages, can point to. Used well, it lets your organisation, your authors and your products be described once and recognised everywhere. Used badly, it quietly merges things that should be separate or splits one thing into many.
The usual culprits are one identifier reused for different entities, shared entities with no identifier at all, and relative identifiers that resolve differently on each page. This guide explains how identifiers behave and gives you a scheme you can apply across a whole site.
What @id actually does
In JSON-LD every entity is a node in a graph. @id is that node's identifier. It is written
as a URL, but it is a name rather than a link: nothing has to load at that address, and search engines
do not need to visit it. What matters is the string itself, because two nodes with exactly the same @id
are treated as the same thing, and their properties are combined into one entity.
That single rule explains every problem in this guide:
- If two different things share an
@id, a processor merges them into one confused entity, for example a product that also appears to be a blog post. - If one thing has two different
@idvalues, it becomes two entities that happen to have the same name, and anything you said about one is not linked to the other. - If a node has no
@id, it becomes a blank node. That is fine for small details such as an address or an offer, but a blank node cannot be referred to from anywhere else, so a shared entity without one has to be copied out in full every time it is used.
The JSON Checker flags identifiers that are duplicated across different entities and shared entities that lack one, and it checks whether references in the markup lead to a defined node. The last of those has its own guide on fixing dangling references; this page is about the identifiers themselves.
The @id problems, and what each one causes
| Problem | Example | Result |
|---|---|---|
| Same @id on different entities | A theme gives both the WebPage and the Article https://www.example.co.uk/blog/wet-rooms/ | One node with two types and a mixture of properties |
| Organisation identified by the home page address | "@id": "https://www.example.co.uk/"
on the Organization, while the WebSite or home WebPage also uses it | The company, the website and the home page merge into one |
| Relative fragment only | "@id": "#organization" | Resolves against each page's address, so every page defines a different organisation |
| Host or protocol drift | http://example.co.uk/#organization on some pages,
https://www.example.co.uk/#organization on others | Two organisations that never connect |
| Tracking parameters in the identifier | An @id built from the current URL, including ?utm_source=newsletter | A new identity for every campaign link |
| Missing on shared entities | Author written out in full on each article with no @id | Many unconnected copies of the same person |
| Generic fragment reused for every item | "@id": "https://www.example.co.uk/#product"
on every product page | Every product on the site claims to be the same product |
The last row is more common than it sounds. It happens when a template hard-codes the site address
plus #product instead of using the page address plus #product. A single product
is described correctly on each page, but across the site the markup says there is only one product with
hundreds of names and prices.
The relative identifier trap
Many hand-written blocks and some older plugins use short identifiers such as "@id": "#organization".
JSON-LD treats that as a relative address and resolves it against the address of the page it appears
on, in the same way a browser resolves a relative link. So the same line of code produces a different
identifier on every page:
| Page | Written | Resolves to |
|---|---|---|
| Home page | #organization | https://www.example.co.uk/#organization |
| Services page | #organization | https://www.example.co.uk/services/#organization |
| A blog post | #organization | https://www.example.co.uk/blog/wet-rooms/#organization |
Within a single page that is harmless, because the definition and any reference to it resolve the
same way. Across a site, it means your business is described as a separate entity on every page. The
fix is simple: write site-wide identifiers in full, starting with https://, so they are
identical wherever they appear.
A naming scheme that holds up
There is no official format for identifiers. What matters is that each is unique to one thing, identical everywhere that thing appears, and never changes. The simplest way to achieve that is to base each identifier on the canonical address of the page most closely associated with the entity, plus a short fragment naming its role.
| Entity | Suggested @id | Scope |
|---|---|---|
| Organization or LocalBusiness | https://www.example.co.uk/#organization | Whole site |
| WebSite | https://www.example.co.uk/#website | Whole site |
| Person (author or staff) | Their profile page plus #person | Whole site |
| Branch of a chain | The branch page plus #localbusiness | Whole site |
| WebPage | The page's canonical address | That page |
| Article, Product, Event, Recipe | The page's canonical address plus #article,
#product and so on | That page |
| BreadcrumbList | The page's canonical address plus #breadcrumb | That page |
Four rules make the scheme reliable:
- Use the canonical address. Take it from the same value as the page's canonical link, not from the address in the browser, so query strings, tracking parameters and alternative hosts never leak in.
- One host, one protocol. Pick the version your site redirects to, such as https with www, and use it in every identifier, even on staging copies that are served from another address.
- Give each role its own fragment. The page and the thing the page is about are different entities. The WebPage takes the bare address; the Article or Product takes the address plus a fragment.
- Never change an identifier once it is live. If a page moves, keeping the old identifier is acceptable because it is only a name. Changing it disconnects everything already recorded about that entity.
The same scheme applied to a blog post looks like this. Note that the WebPage, the article, the organisation and the author each have their own identifier, and the article points to the others by identifier instead of repeating them.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://www.example.co.uk/#organization",
"name": "Hollin Lane Bathrooms",
"url": "https://www.example.co.uk/",
"logo": "https://www.example.co.uk/images/hollin-lane-logo.png"
},
{
"@type": "WebSite",
"@id": "https://www.example.co.uk/#website",
"url": "https://www.example.co.uk/",
"name": "Hollin Lane Bathrooms",
"publisher": { "@id": "https://www.example.co.uk/#organization" }
},
{
"@type": "Person",
"@id": "https://www.example.co.uk/team/priya-hale/#person",
"name": "Priya Hale",
"url": "https://www.example.co.uk/team/priya-hale/"
},
{
"@type": "WebPage",
"@id": "https://www.example.co.uk/blog/wet-rooms/",
"url": "https://www.example.co.uk/blog/wet-rooms/",
"name": "Planning a Wet Room in an Older House",
"isPartOf": { "@id": "https://www.example.co.uk/#website" }
},
{
"@type": "BlogPosting",
"@id": "https://www.example.co.uk/blog/wet-rooms/#article",
"headline": "Planning a Wet Room in an Older House",
"datePublished": "2026-06-11T10:00:00+01:00",
"mainEntityOfPage": { "@id": "https://www.example.co.uk/blog/wet-rooms/" },
"author": { "@id": "https://www.example.co.uk/team/priya-hale/#person" },
"publisher": { "@id": "https://www.example.co.uk/#organization" }
}
]
}
</script>
The fix, step by step
Step 1
Collect the identifiers from each template
Open one page built from each template, such as the home page, an article, a product,
a category, a contact page and a team profile, and copy every @id value into a spreadsheet
with the page address, the entity type and the tool that wrote it. Include blocks from the theme, SEO
plugin, shop platform and any apps. Seeing the identifiers side by side is the quickest way to spot drift,
duplicates and fragments that change from page to page.
Step 2
Choose one canonical form for the base address
Decide on the single protocol and host that your site redirects to, for example https://www.example.co.uk,
and whether page addresses end with a slash. Then check every identifier in your spreadsheet against
it. Anything using http, the bare domain, a staging host, a trailing slash difference or a query string
needs changing, because each of those small differences creates a separate identity in the eyes of any
processor reading your markup.
Step 3
Give each shared entity a single site-wide @id
For the organisation, the website, each author and each branch, choose one absolute identifier and write it in full on every page, never as a relative fragment. If your SEO plugin already outputs identifiers for these entities, adopt the plugin's values in any custom code instead of inventing new ones, so the two sources describe the same nodes. Record the final identifiers somewhere your developers can copy from, so nobody types them from memory later.
Step 4
Make page-level identifiers unique to their page
For entities that belong to one page, such as the WebPage, the article, the product
and the breadcrumb, build the identifier from that page's canonical address plus a role fragment. Replace
any template that hard-codes the site root plus #product or #article, because
that makes every page claim the same entity. Take the address from the same variable as the canonical
tag, so parameters and alternative hosts cannot slip into the identifier.
Step 5
Separate entities that share an identifier by mistake
Where two different things carry the same @id, such as a WebPage
and its Article, or an Organization and the home page, give one of them a new fragment and update any
references that pointed to it. Keep a shared identifier only where the two nodes really describe the
same thing, for example the organisation defined by your SEO plugin and extended by your own block. Check
that the merged result has no contradictory values.
Step 6
Retest pages from every template
Clear page and plugin caches, then run the JSON Checker on each template again and compare the new identifiers with your spreadsheet. The organisation and website identifiers should be identical on every page, while page-level identifiers should all differ. Confirm in the Schema Markup Validator that each entity appears once with the properties you expect, and repeat the check after theme, plugin or platform updates, which sometimes reset identifier formats.
When two blocks share an @id on purpose
Reusing an identifier is not always a mistake. It is the correct way to say "this is the same thing"
across blocks, and it lets you add to an entity that another tool defines without replacing it. If your
SEO plugin describes the organisation with https://www.example.co.uk/#organization, your
own block can add opening hours or a sameAs list to the same node by using the same identifier.
Two cautions apply. First, the combined node must still make sense: if the plugin says the organisation
is called one thing and your block says another, the entity ends up with two names. That is the subject
of the guide on fixing conflicting schema
blocks. Second, if the two blocks give the node different types, it becomes both types. That is intended
when a business is genuinely a LocalBusiness and an Organization, but not when
a theme labels the home page as an organisation.
Not every tool merges nodes across separate script blocks on one page in the same way. Placing the
pieces in one @graph where you control the output removes any doubt; where you cannot, identical
identifiers are still the best signal you can give.
Identifiers on common platforms
Most SEO plugins and shop platforms generate identifiers automatically, and it is usually better to work with their format than against it. Formats change between versions, so always read the live output rather than relying on documentation or this summary.
- WordPress with Yoast SEO has typically used the site address plus
#organizationand#website, the page address for the WebPage, and the page address plus#articlefor posts. People get identifiers under a#/schema/person/path. Custom code should reuse these values. - WordPress with Rank Math has typically used site-level fragments such as
#organizationand#websiteon the full site address, and page-level fragments such as#webpage. Check the output of your version before copying the pattern. - Shopify themes vary widely. Many output Product and Organization blocks without identifiers at all, which means a review app or SEO app adding its own Product cannot be linked to the theme's. Adding page-based identifiers in the theme's Liquid is often the cleanest fix.
- Static sites and frameworks should build identifiers from a single configured base address, never from the request address, so preview deployments and local builds do not produce stray hosts.
1. Does the @id address have to be a real page?
No. An @id is a name for an entity, written in the form of a web address so that it is globally unique. Nothing needs to load at that address, and search engines are not expected to visit it. That said, basing identifiers on real canonical page addresses is sensible, because it guarantees uniqueness, makes them easy to read, and ties each entity to the page most closely about it. Just make sure the same entity always gets exactly the same string.
2. Why is #organization on its own a problem?
A bare fragment is a relative address, so JSON-LD resolves it against the address of the page it is on. On the home page it becomes the home address plus the fragment; on the contact page it becomes the contact address plus the fragment. Your organisation therefore gets a different identifier on every page, and the separate copies never connect. Write site-wide identifiers in full, starting with https and your chosen host, so they are identical everywhere they appear.
3. Should the WebPage and the Article have the same @id?
No. The page is the document at an address, while the article is the piece of writing it contains, and schema.org treats them as separate entities with different properties. Give the WebPage the canonical address and the Article the same address plus a fragment such as #article, then link them with mainEntityOfPage on the article or mainEntity on the page. Sharing one identifier merges the two into a single node that carries both types and a jumble of properties.
4. Do small nested objects like addresses and offers need an @id?
Usually not. An address, an offer, a rating or an opening hours specification normally belongs to one entity and is never referred to from anywhere else, so leaving it as a nested object without an identifier is correct and keeps the markup simpler. Identifiers matter for things that are shared or referred to, such as your organisation, your website, people, and the main entity of each page. Adding identifiers everywhere adds maintenance without adding meaning.
5. I changed SEO plugin and all my @id values changed. Is that harmful?
It is not harmful in the sense of a penalty, and search engines will pick up the new identifiers as they recrawl. The risk is breakage around the edges: custom code, a shop app or a hand-written block that still refers to the old identifiers now points at nothing. After a switch, search your theme, snippets and apps for the old identifier strings and update them, then run the JSON Checker across each template to confirm every reference resolves.
6. Can two separate script blocks describe the same entity with one @id?
Yes, and that is the proper way to extend an entity defined by a plugin you cannot edit. Both blocks use the identical @id and each contributes properties. Keep the values consistent, because contradictions in the combined node are treated as a conflict. Not every tool combines nodes across separate script blocks identically, so where you control all the markup, putting everything into a single @graph is the more predictable arrangement and easier to check by eye.
7. Should my @id include www or not?
It should match whichever version your site redirects to and uses in its canonical tags. If the live site is https with www, every identifier should use https with www. The important point is consistency: mixing the bare domain and the www version, or http and https, creates two identities for the same thing. Pick one, build identifiers from the same setting the canonical tag uses, and check staging environments do not leak their own host into the markup.
The JSON Checker parses every JSON-LD block on your page, validates the syntax, checks each type against schema.org, verifies required fields, detects conflicting types and tests the result against Google rich result criteria.
Open JSON-LD Fixes โ