14+ years building on WordPress / Replies in under 5 hours
Headless WordPress 7 min read · Updated July 2026

How to Merge Multiple JSON-LD Schema Scripts for Better SEO

AK
Ajay Khandal
WordPress Developer
How to Merge Multiple JSON-LD Schema Scripts for Better SEO
TL;DR

Wrap multiple schema types in a single @graph array within one <script type="application/ld+json"> block. Assign each node an absolute @id URL (e.g. https://yourdomain.com/#organization), then reference that @id from other nodes — your BlogPosting.publisher becomes {"@id": "https://yourdomain.com/#organization"} instead of repeated Organization data. Rank Math and Yoast both output this @graph structure by default. To add custom schema alongside Rank Math, use the rank_math/json_ld filter to inject nodes into the existing graph rather than adding a competing separate script. Test with Google Rich Results Test (search.google.com/test/rich-results) and validator.schema.org.

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.

Frequently asked questions

No. Google's documentation explicitly confirms that multiple JSON-LD scripts on a single page are valid and parsed correctly. The case for merging into a single @graph block is entity relationship linking, not a technical requirement. Without @graph and @id, each script is an isolated statement — Google can read them but cannot unambiguously connect the Organization in one script to the Article publisher reference in another. @graph makes those relationships explicit.

The @id property assigns a unique identifier to a schema node — an absolute URL that acts as the node's permanent name within the graph. Once a node has an @id, other nodes can reference it by that ID without repeating its properties. For example, an Organization node with @id 'https://yourdomain.com/#organization' can be cited as a BlogPosting's publisher using just {"@id": "https://yourdomain.com/#organization"} — a reference, not a duplicate definition.

Use the @graph property at the root of your JSON-LD object. @graph takes an array of schema nodes, each of which can have its own @type, @id, and properties. The @context property ("https://schema.org") is declared once at the root alongside @graph — it does not need to be repeated inside individual nodes. Each node in the @graph array should have a unique @id following the https://yourdomain.com/#entity-name convention.

Define the Organization node with an @id value (e.g. "@id": "https://yourdomain.com/#organization"). Then in the BlogPosting node, set the publisher field to a reference object: "publisher": {"@id": "https://yourdomain.com/#organization"}. This tells Google the publisher is that specific Organization node, rather than a generic Organization with the same name. The same @id reference works for the author field if the author is the same entity.

Yes. Rank Math generates a single @graph block on every page containing Organization, WebSite, WebPage, and BreadcrumbList nodes — all linked via @id references. On posts, it adds an Article node. To add custom schema types (like FAQPage) alongside Rank Math's output, use the rank_math/json_ld PHP filter to inject additional nodes into the existing @graph array rather than adding a separate block. Yoast SEO also outputs @graph by default.

Use two tools: Google Rich Results Test (search.google.com/test/rich-results) shows which rich result types your page qualifies for and displays the fully resolved entities — including where @id references were followed to their full definition. Schema.org Validator (validator.schema.org) validates the JSON-LD structure against the Schema.org specification and catches structural issues like invalid property names or malformed @id values. Paste the raw JSON-LD directly into either tool to isolate schema issues from page rendering issues.

AK

Written by Ajay Khandal

WordPress Developer — building, fixing and speeding up WordPress sites.

Work with me →