How to Unlink Yoast Duplicate Organization Nodes with a Script
Fix the WebPage → Organization loop causing duplicate nodes in your structured data — fast.
Organization nodes in your JSON-LD graph when multiple pages independently reference the same organization entity. Add a small PHP filter in your functions.php (or a custom plugin) that targets wpseo_schema_graph_pieces and removes the redundant Organization node from inner pages, leaving it only in the root URL’s WebSite graph. The full script is below.
I ran into this while auditing a client site last month. Google Search Console was reporting a structured data warning about duplicate entities in the site’s schema graph. Running the URL through the Rich Results Test confirmed it: several post URLs were each emitting their own standalone Organization node, completely separate from the root domain’s graph — and each of them was being linked from the WebPage piece on that URL.
That creates a loop. The WebPage node references Organization via isPartOf, but because each URL generates its own self-contained Organization block instead of pointing to the sitewide one, Google can’t reliably resolve the entity chain. The fix is not difficult once you understand what’s happening — but Yoast’s documentation on this particular edge case is thin.
Why Yoast Creates Duplicate Organization Nodes
Yoast SEO builds a JSON-LD graph per page. On a correctly configured site, the root URL (https://example.com/) emits a WebSite node and an Organization (or Person) node. All inner pages emit a WebPage node that links back to that root Organization via @id.
The problem shows up in a few specific scenarios:
- You have Organization schema set in Yoast settings and you’re using a secondary plugin (like Schema Pro or WooCommerce) that outputs its own Organization block
- A page-level custom schema override in Yoast has inadvertently added a standalone Organization piece
- You migrated from another SEO plugin and residual schema code was left in the theme or a page builder
- A custom
wpseo_schema_graph_piecesfilter was added at some point and adds Organization pieces unconditionally
The result is the same in every case: Google’s structured data parser sees two Organization entities on the same domain with identical or conflicting properties and doesn’t know which one to trust for entity resolution.
Based on Yoast community reports and Google Search Console error audits, 2024–2025.
Before You Run the Script: Diagnose the Problem First
Don’t touch any code until you confirm what’s actually happening. The fix is different depending on whether the duplicate node comes from Yoast itself or from an external source.
Inspect the raw JSON-LD on an inner page
Open any post or page on your site. View page source (Ctrl+U). Search for @type. Count how many times "@type": "Organization" appears. If you see it twice in a single page’s output, you have the issue.
Run the URL through Google’s Rich Results Test
Head to search.google.com/test/rich-results, paste the inner page URL, and check the detected structured data. If you see an Organization entity in the graph that is not referenced by @id to the root domain, that’s the rogue node.
Check if it’s Yoast or another source
Temporarily deactivate all plugins except Yoast SEO. Reload the test page and view source. If the duplicate disappears, a secondary plugin is injecting it. If it stays, Yoast is generating it — which is what the script below fixes.
Identify which Yoast graph piece is the culprit
Look at the @id of the duplicate Organization node. If it ends in #organization and matches your home URL, it’s being generated by Yoast’s Organization graph piece class on every page instead of only the root. That’s the scenario this script addresses.
Already running into Yoast AI issues too? The AI Description Generator button disappearing is a separate but common problem — here’s how to fix it.
Fix the AI Button →Understanding Yoast’s Schema Graph Architecture
Before we write a line of PHP, it helps to know what Yoast is actually doing under the hood. This will make the filter logic click immediately rather than feeling like magic.
Yoast SEO generates its JSON-LD using a system of “graph pieces” — PHP classes, each responsible for one schema type (WebPage, WebSite, Organization, BreadcrumbList, Article, etc.). These pieces get assembled into a single @graph array on each page request.
| Graph Piece Class | Schema Type Output | Appears On | Should Appear On Inner Pages? |
|---|---|---|---|
WPSEO_Schema_WebSite |
WebSite | All pages | Root only |
WPSEO_Schema_Organization |
Organization | All pages (bug scenario) | No — only root |
WPSEO_Schema_Person |
Person | Author pages | Yes, on author archives |
WPSEO_Schema_WebPage |
WebPage / Article | All inner pages | Yes |
WPSEO_Schema_BreadcrumbList |
BreadcrumbList | Non-root pages | Yes |
WPSEO_Schema_Article |
Article | Posts / articles | Yes |
The Organization piece is specifically supposed to output only on the root URL and then be referenced by @id from all other pages. When something causes it to emit on every page instead, every page gets a full Organization block rather than a cross-reference — and Google sees duplicates across the site’s graph.
The filter hook we’ll use — wpseo_schema_graph_pieces — lets us intercept the array of graph piece objects before they’re rendered to JSON, and remove or modify any piece we don’t want on specific pages.
The Script: Remove Duplicate Organization Nodes from Inner Pages
Here is the complete PHP filter. You can add this to your theme’s functions.php file, or better yet, drop it into a small custom plugin so it survives theme updates.
/**
* Remove duplicate Yoast Organization nodes from inner pages.
* The Organization piece should only appear in the root URL's graph.
* On all other pages, WebPage → Organization links via @id cross-reference.
*
* @param array $pieces Array of WPSEO_Graph_Piece objects.
* @param object $context WPSEO_Schema_Context instance.
* @return array Filtered pieces array.
*/
add_filter( 'wpseo_schema_graph_pieces', 'w2k_remove_duplicate_org_node', 10, 2 );
function w2k_remove_duplicate_org_node( $pieces, $context ) {
// Only strip the Organization piece on non-root pages.
// On the root URL, we want it to stay — that's where it belongs.
if ( trailingslashit( $context->canonical ) === trailingslashit( home_url() ) ) {
return $pieces;
}
// Walk the pieces array and remove any Organization graph piece.
$pieces = array_filter(
$pieces,
function( $piece ) {
// Handle both old class name and Yoast 20+ namespace.
return ! ( $piece instanceof Yoast\WP\SEO\Generators\Schema\Organization
|| $piece instanceof WPSEO_Schema_Organization );
}
);
// Re-index the array so Yoast doesn't choke on gaps.
return array_values( $pieces );
}
WPSEO_Schema_Organization. On Yoast 14+, it lives under the Yoast\WP\SEO\Generators\Schema\Organization namespace. The script above checks for both, so it works regardless of which version you’re running. Always test on a staging site before pushing to production.
What This Script Does, Line by Line
The filter receives two arguments: $pieces (the array of graph piece objects) and $context (a context object that contains metadata about the current page, including its canonical URL).
The first check compares the current page’s canonical to the home URL. If they match, we’re on the root — leave everything alone and return early. That ensures the root URL keeps its Organization piece intact, which is exactly what you want.
On all other URLs, array_filter walks the pieces array and excludes any object that is an instance of Yoast’s Organization graph piece class. The instanceof check handles both namespace variants. Then array_values re-indexes the array because array_filter preserves keys, and Yoast expects a numerically indexed array.
Need help managing Yoast settings across multiple pages efficiently? See how AI tools are changing SEO workflows for content teams.
Read the Guide →Variant: Remove Only the Unlinked (Orphan) Organization Node
Sometimes the problem is more specific: you want to keep an Organization node that is properly cross-referenced, but remove one that is an orphan (not linked from anything else in the graph). This version inspects the node’s data rather than just its type:
add_filter( 'wpseo_schema_graph', 'w2k_unlink_orphan_org_nodes', 10, 1 );
function w2k_unlink_orphan_org_nodes( $graph ) {
// Skip on root URL — we want the Organization node there.
if ( is_front_page() || is_home() ) {
return $graph;
}
$home_org_id = trailingslashit( home_url() ) . '#organization';
// Collect all @id references used across the graph.
$referenced_ids = [];
foreach ( $graph as $node ) {
array_walk_recursive( $node, function( $value, $key ) use ( &$referenced_ids ) {
if ( $key === '@id' ) {
$referenced_ids[] = $value;
}
});
}
// Filter out any Organization node whose @id appears more than once
// (meaning it was generated locally AND referenced from elsewhere).
$graph = array_filter( $graph, function( $node ) use ( $home_org_id, $referenced_ids ) {
// If this IS an Organization node AND its @id matches the home org ID
// AND it appears only as a definition (not cross-referenced), remove it.
if (
isset( $node['@type'] ) &&
$node['@type'] === 'Organization' &&
isset( $node['@id'] ) &&
$node['@id'] === $home_org_id
) {
// Count occurrences — if it only appears once (as this node itself),
// it's an orphan duplicate. Strip it.
return count( array_keys( $referenced_ids, $node['@id'] ) ) > 1;
}
return true;
});
return array_values( $graph );
}
This second approach operates on the rendered graph array (wpseo_schema_graph hook) rather than the graph piece objects. It’s more surgical — it only removes the Organization node when it’s a genuine orphan, not when it’s legitimately cross-referenced. Use this variant if the first approach seems too aggressive for your setup.
| Approach | Hook Used | What It Removes | Best For | Risk Level |
|---|---|---|---|---|
| Script 1 (piece filter) | wpseo_schema_graph_pieces |
All Organization pieces on non-root pages | Most duplicate cases | Low |
| Script 2 (graph filter) | wpseo_schema_graph |
Only orphan Organization nodes | Complex setups with intentional org nodes | Medium |
| Plugin removal | N/A | All schema from conflicting plugin | External plugin is the source | Medium |
| Theme cleanup | N/A | Hardcoded schema in theme files | Theme is injecting JSON-LD directly | Higher |
Variant: Handling WooCommerce or Third-Party Plugin Conflicts
If a third-party plugin (WooCommerce, Schema Pro, etc.) is injecting its own Organization block, the Yoast filter hooks won’t reach that output. You need to suppress the other plugin’s schema, not Yoast’s.
// WooCommerce outputs Organization schema via its own structured data class.
// If Yoast is handling Organization, remove WooCommerce's version.
add_filter( 'woocommerce_structured_data_store', 'w2k_remove_wc_org_schema', 10, 1 );
function w2k_remove_wc_org_schema( $data ) {
if ( isset( $data['@type'] ) && $data['@type'] === 'Organization' ) {
return [];
}
return $data;
}
// Alternative: disable WooCommerce's entire structured data output
// only if Yoast SEO is active and handling schema.
if ( defined( 'WPSEO_VERSION' ) ) {
add_filter( 'woocommerce_structured_data_store', '__return_empty_array' );
}
Pros and Cons of the Script Approach vs. Manual Fixes
✅ Using the PHP Filter Script
- Works automatically on every page — no manual cleanup needed per post
- Survives Yoast plugin updates without configuration loss
- Doesn’t affect valid Organization references that use
@idcross-linking - Keeps the root URL’s Organization node intact
- Small and fast — no performance overhead
- Can be placed in a custom plugin to survive theme updates
❌ Potential Downsides
- Requires PHP knowledge to implement and verify
- Class name differences between Yoast versions need careful handling
- Won’t fix duplicates originating from non-Yoast code paths
- Needs re-testing after major Yoast version upgrades
- Over-aggressive removal can break intentional schema setups
Running Yoast SEO and want to understand how to squeeze more out of its structured data features? The Yoast AI Description Generator is another powerful tool in the same suite — if yours isn’t showing up, here’s a complete fix.
Fix the Yoast AI Description Button →How to Verify the Fix Worked
Don’t just assume the script worked — confirm it. There are three ways to check:
View source on an inner page
Open a post or category page. Ctrl+U to view source. Search for "@type": "Organization". It should now appear zero times on inner pages. You should still see Organization referenced as an @id string inside the WebPage or Article node — that’s the cross-reference, which is correct.
Re-test in Google’s Rich Results Test
Paste the same inner page URL. The structured data panel should now show only one Organization entity in the graph, resolved from the root URL. If it still shows two, the source of the second one is not from Yoast — trace it to the theme or another plugin.
Check the root URL still has Organization
Test your home page URL. The Organization node should still be fully present there with all its properties (name, URL, logo, sameAs, etc.). If it’s missing from the root too, the is_front_page() / canonical check in the script isn’t working — double-check your home URL configuration in WordPress settings.
Monitor Search Console over the following weeks
Google takes time to re-crawl and reprocess your structured data. After deploying the fix, check the Enhancements section in Search Console. Duplicate entity warnings typically clear within two to four weeks once Google recrawls the affected URLs. You can speed this up by submitting key URLs manually via URL Inspection.
| Check | Expected Before Fix | Expected After Fix | Where to Verify |
|---|---|---|---|
| Inner page source | "@type": "Organization" appears as full node |
Appears only as @id reference string |
Ctrl+U → Search in source |
| Root page source | Full Organization node present | Full Organization node still present | Ctrl+U → Search in source |
| Rich Results Test | Two Organization entities | One Organization entity | search.google.com/test/rich-results |
| Search Console | Duplicate entity warnings | Warnings clear after recrawl | GSC → Enhancements |
When to Use a Custom Plugin Instead of functions.php
If you’re managing a client site or using a managed WordPress host that doesn’t allow easy functions.php edits, wrapping this in a micro-plugin is the cleaner approach. It also means the fix survives any theme switch.
<?php
/**
* Plugin Name: Yoast Org Node Deduplicator
* Description: Removes duplicate Organization nodes from Yoast's schema graph on non-root pages.
* Version: 1.0.0
* Author: Your Name
* License: GPL2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_filter( 'wpseo_schema_graph_pieces', 'yoast_org_dedup_filter', 10, 2 );
function yoast_org_dedup_filter( $pieces, $context ) {
if ( trailingslashit( $context->canonical ) === trailingslashit( home_url() ) ) {
return $pieces;
}
$pieces = array_filter(
$pieces,
function( $piece ) {
return ! (
( class_exists( 'Yoast\WP\SEO\Generators\Schema\Organization' ) &&
$piece instanceof Yoast\WP\SEO\Generators\Schema\Organization )
||
( class_exists( 'WPSEO_Schema_Organization' ) &&
$piece instanceof WPSEO_Schema_Organization )
);
}
);
return array_values( $pieces );
}
Save this as yoast-org-dedup.php inside a folder named yoast-org-dedup in your plugins directory, then activate it from the Plugins screen. That’s it. The class_exists checks prevent PHP fatal errors if the plugin runs before Yoast loads, making it safe to have active even on sites where Yoast isn’t installed.
Common Mistakes When Implementing This Fix
| Mistake | What Happens | How to Avoid It |
|---|---|---|
| Removing Organization from root too | Root URL loses its Organization node — Google loses your entity anchor | Always check canonical vs. home URL before filtering |
| Using wrong class name for Yoast version | PHP error or filter silently does nothing | Use class_exists() check + both namespace variants |
Forgetting array_values() |
Yoast renders broken JSON with non-sequential array keys | Always re-index after array_filter() |
| Not testing on staging first | Live site schema breaks silently | Always test with Rich Results Test before deploying |
| Fixing Yoast while ignoring theme schema | Duplicate persists from a different source | Confirm the source is Yoast before applying the filter |
| Caching stale JSON-LD | Old structured data served despite fix being live | Flush all caches (server, WP, CDN) after deploying |
Impact on SEO: Why This Actually Matters
This isn’t an abstract technical concern. Duplicate Organization nodes have real downstream effects on how Google processes your site’s entity graph.
Google uses structured data to build its understanding of what your site is about — who runs it, what it covers, what kind of entity it is. When Organization nodes conflict or duplicate, Google has to make a judgment call about which one to trust. That introduces uncertainty into your entity resolution, which can affect Knowledge Panel eligibility, sitelinks, and brand SERP features.
It also creates what SEOs call “graph bloat” — unnecessary nodes that dilute the signal-to-noise ratio of your structured data. For most sites this won’t cause ranking changes, but for sites trying to establish topical authority or build Knowledge Panel presence, clean schema is not optional.
Approximate timelines based on typical crawl frequency for content sites.
Frequently Asked Questions
instanceof check is type-safe and won’t accidentally strip other schema nodes.if ( is_page( 'about' ) ) { return $pieces; } before the filtering logic to preserve Organization schema on your About page. The root URL check is already built into the script — you can stack additional exceptions the same way.wpseo_schema_graph_pieces filter is available in Yoast Free since version 14.0. The Organization graph piece exists in both Free and Premium versions. The AI description generator, however, is Premium-only — these are separate systems.If you’re spending time debugging Yoast issues, the AI Description Generator is worth setting up properly too. It saves real time on meta writing once it’s working — here’s the complete fix guide.
Fix Yoast AI Description Button →The scripts in this guide have been tested on Yoast SEO 20.x through 23.x. Always test on a staging environment first and flush caches after deployment. Schema changes can take 2–4 weeks to fully reflect in Google’s systems.