Most WordPress sites end up with multiple JSON-LD <script> blocks in the page source — one for the Organization, one from Rank Math for the Article, another added manually for FAQs. This happens naturally when plugins inject their own structured data independently.
The @graph property solves a specific problem this creates. Without it, each JSON-LD block is an isolated statement. Google knows there is an Organization and there is a BlogPosting, but not that the BlogPosting was published by that specific Organization. The @id property is how you make those relationships explicit — and how Rank Math and Yoast both structure their output by default.
Multiple scripts vs @graph: what actually changes
Google’s own documentation confirms that multiple JSON-LD scripts on a single page are valid and parsed correctly — there is no crawl budget penalty or technical problem with the fragmented approach. The case for @graph is not performance. It is entity linking.
Without @graph, if you define your Organization in one script and reference it in another as "publisher": {"@type": "Organization", "name": "Ajay Khandal"}, Google treats that as a description of some organization named Ajay Khandal. It may or may not connect it to the Organization defined separately.
With @graph and @id, you assign a unique identifier to the Organization node: "@id": "https://ajaykhandal.com/#organization". Then in the BlogPosting node, you write "publisher": {"@id": "https://ajaykhandal.com/#organization"}. Google now knows: the publisher of this Article is that specific entity — the one defined in the graph with all its associated properties (logo, URL, sameAs links). The relationship is explicit, not inferred.
The @id convention: how node identifiers work
@id values in JSON-LD must be absolute URLs. The standard convention for entity identifiers uses the site’s canonical URL with a fragment:
- Organization:
https://yourdomain.com/#organization - WebSite:
https://yourdomain.com/#website - WebPage (page-specific):
https://yourdomain.com/page-slug/#webpage - Article (post-specific):
https://yourdomain.com/post-slug/#article - BreadcrumbList:
https://yourdomain.com/post-slug/#breadcrumb
The fragment part (#organization, #article) is an arbitrary but stable identifier — what matters is that each entity has a unique, absolute URL. Rank Math uses exactly this pattern. Yoast uses slightly different fragment names but the same principle.
Once defined with an @id, other nodes in the @graph reference it without repeating properties:
"publisher": {
"@id": "https://yourdomain.com/#organization"
}
This is a reference, not a definition. Google follows the @id to the full Organization node and reads its properties from there.
The fragmented approach vs the @graph approach
Here is the same structured data written both ways. The fragmented version:
<!-- Script 1 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Ajay Khandal",
"url": "https://ajaykhandal.com"
}
</script>
<!-- Script 2 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Example Post Title",
"publisher": {
"@type": "Organization",
"name": "Ajay Khandal"
}
}
</script>
The two scripts are isolated statements. Google knows there is an Organization and a BlogPosting. Whether they are related is an inference.
The merged @graph version:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://ajaykhandal.com/#organization",
"name": "Ajay Khandal",
"url": "https://ajaykhandal.com",
"logo": {
"@type": "ImageObject",
"@id": "https://ajaykhandal.com/#logo",
"url": "https://ajaykhandal.com/wp-content/uploads/logo.png"
}
},
{
"@type": "BlogPosting",
"@id": "https://ajaykhandal.com/example-post/#article",
"headline": "Example Post Title",
"author": {
"@id": "https://ajaykhandal.com/#organization"
},
"publisher": {
"@id": "https://ajaykhandal.com/#organization"
}
}
]
}
</script>
The BlogPosting’s author and publisher fields are now explicit references to the Organization node. Neither repeats the Organization’s name, URL, or logo — they point to the full definition by @id.
Adding BreadcrumbList to the graph
BreadcrumbList is a page-specific node. It belongs in the same @graph as the Article, connected to it via a breadcrumb reference:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://ajaykhandal.com/#organization",
"name": "Ajay Khandal",
"url": "https://ajaykhandal.com"
},
{
"@type": "WebSite",
"@id": "https://ajaykhandal.com/#website",
"url": "https://ajaykhandal.com",
"publisher": { "@id": "https://ajaykhandal.com/#organization" }
},
{
"@type": "BreadcrumbList",
"@id": "https://ajaykhandal.com/merge-json-ld-schema-scripts-seo/#breadcrumb",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://ajaykhandal.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "How to Merge JSON-LD Schema Scripts"
}
]
},
{
"@type": "BlogPosting",
"@id": "https://ajaykhandal.com/merge-json-ld-schema-scripts-seo/#article",
"headline": "How to Merge Multiple JSON-LD Schema Scripts for Better SEO",
"author": { "@id": "https://ajaykhandal.com/#organization" },
"publisher": { "@id": "https://ajaykhandal.com/#organization" },
"isPartOf": { "@id": "https://ajaykhandal.com/#website" },
"breadcrumb": { "@id": "https://ajaykhandal.com/merge-json-ld-schema-scripts-seo/#breadcrumb" }
}
]
}
</script>
The isPartOf property connects the Article to the WebSite node. The breadcrumb property connects it to the BreadcrumbList. Neither repeats the WebSite or BreadcrumbList data — they reference the appropriate @id.
WordPress plugins: Rank Math and Yoast output @graph automatically
Rank Math generates a @graph structure by default on every page. View the source of any Rank Math-powered WordPress page and you’ll find a single JSON-LD block containing Organization, WebSite, WebPage, BreadcrumbList, and (on posts) Article — all linked via @id references. The fragment identifiers follow the #organization, #website, #webpage convention exactly as described above.
Yoast SEO does the same. Both plugins generate the full @graph automatically without manual configuration.
If you need to add a FAQPage or other custom schema to a post that uses Rank Math, inject it into the existing @graph via the rank_math/json_ld filter rather than adding a separate <script> block:
add_filter( 'rank_math/json_ld', function( $data ) {
$data['FAQPage'] = [
'@type' => 'FAQPage',
'@id' => get_permalink() . '#faqpage',
'mainEntity' => [
[
'@type' => 'Question',
'name' => 'What is @graph in JSON-LD?',
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => 'The @graph property holds multiple schema nodes in one JSON-LD block.',
],
],
],
];
return $data;
} );
This merges the FAQPage into Rank Math’s existing @graph output. Adding a separate <script> block for the FAQ would create a competing, unlinked statement — defeating the entity relationship linking the @graph approach provides.
Schema configuration is one of the items to verify at go-live. The advanced WordPress launch checklist covers structured data verification alongside wp-config.php, caching, and security headers.
Testing merged JSON-LD schema
Two tools validate JSON-LD for different purposes:
Google Rich Results Test (search.google.com/test/rich-results) shows which rich result types your page qualifies for based on the structured data Google finds. It reads the page as Googlebot and extracts schema entities. Use it to confirm the merged @graph is parsed correctly and that entity types (Article, FAQPage, BreadcrumbList) are detected. The “Detected Items” panel shows fully resolved entities — including where @id references were followed.
Schema.org Validator (validator.schema.org) validates the JSON-LD structure against the Schema.org specification, independently of Google’s rich result eligibility. Use it to catch structural issues: invalid property names, missing required properties, incorrect nesting, or malformed @id values.
After merging, paste the JSON-LD directly into either tool (not the page URL) to isolate schema issues from page rendering issues. Confirm that cross-references resolve: the Article.publisher should show the full Organization data, not just an unresolved @id pointer.
Structured data is one layer of a wider approach to making content discoverable by AI search engines and answer engines. The generative engine optimization guide covers how named entities, structured facts, and clear content hierarchy affect AI citation and discovery beyond traditional rich results.
Common mistakes with @graph
@graph placed inside a @type instead of at root. The @graph array must be a direct property of the root JSON object. If you see "@type": "WebPage", "@graph": [...], the @graph is in the wrong position. It must appear at the same level as "@context".
Duplicate @id values. Every node in @graph must have a unique @id. If two nodes share the same identifier, parsers take one definition and discard the other — often silently.
Relative URLs in @id. The value "@id": "/#organization" is not valid. @id values must be absolute URLs: "@id": "https://yourdomain.com/#organization".
Injecting a competing script alongside a plugin’s @graph. If Rank Math is already outputting a @graph and you add a separate <script> for a FAQPage, you now have two disconnected statements again. Merge into the plugin’s graph via the rank_math/json_ld filter (or Yoast’s equivalent) instead.
Missing @context at root. The "@context": "https://schema.org" declaration must appear at the root of the JSON-LD object alongside @graph. It does not need to be repeated inside individual nodes within the graph.
Using the wrong schema type for the content. BlogPosting and Article are both valid for blog posts — Google treats them similarly — but mixing them inconsistently across pages creates confusion in how search engines categorize the content. Pick one and use it consistently across all posts.


