Fixing Duplicate Schema From the AI Generator
Almost every site already has some structured data before anyone runs a generator. An SEO plugin writes Article and Organization, a Shopify theme writes Product, a review app adds ratings, and a page builder may add its own. When a generated block is pasted on top, the page ends up describing the same thing twice, usually with different details.
This guide shows how to find every block on a page, work out which source should own each type, and remove or merge the rest so one clear description remains.
How generated markup ends up doubled
The generator reads one page and writes a complete description of it. It cannot see that your plugin already describes the same page, so it writes its own version of everything it thinks belongs there. Duplicates then arrive by a few routes.
- Generated Article on a plugin-managed post. Yoast SEO, Rank Math and similar plugins output an Article or BlogPosting for posts by default. A generated Article pasted into the post adds a second one.
- Generated Product on a shop page. Many Shopify themes and WooCommerce itself output Product markup on product pages. A generated Product adds a second, and a review app often adds a third with its ratings.
- Site-wide entities repeated per page. The generator may include an Organization, WebSite or BreadcrumbList in its output. Your plugin already writes these on every page.
- The same page generated twice. Someone regenerates markup after an edit and pastes the new block without removing the old one, or pastes it once into the content and once into a header scripts field.
- Tag managers. A block added through Google Tag Manager or another script loader is invisible in the page source, so it is easy to forget and add again elsewhere.
What a clash looks like
A recipe blog on WordPress, fictional Hedgerow Kitchen, has this from its SEO plugin:
{ "@type": "Article", "headline": "Blackberry and apple crumble",
"author": { "@id": "https://example.co.uk/#/schema/person/1" },
"datePublished": "2026-08-30T08:00:00+01:00" }
And this from a generated block pasted into the post:
{ "@type": "BlogPosting", "headline": "The best blackberry and apple crumble",
"author": { "@type": "Person", "name": "Hedgerow Kitchen Team" },
"datePublished": "2026-08-30" }
Two articles, two headlines, two different authors. Search engines have to decide which to believe, and neither of them is the Recipe markup that would actually help the page.
Why it matters, and what it does not do
Having several JSON-LD blocks on a page is normal and allowed. Google reads them all, and a page can quite properly carry a WebPage, a BreadcrumbList, an Organization and a Recipe from different sources. Google does not penalise duplicate markup as such. The trouble comes when two blocks describe the same thing and disagree.
| Symptom | Cause |
|---|---|
| Rich Results Test lists two Product or two Article items | Two sources describe the page's main entity separately |
| Warnings on one item and a clean pass on the other | One block is complete and one is a partial copy |
| Search results show a different price or rating from the page | An older generated block carries values the live data has moved on from |
| Two organisations with slightly different names | The generator wrote its own Organization instead of linking to the plugin's |
| Merchant Center price mismatch warnings | The feed matches one Product block and Google read the other |
In short, duplicates waste the effort of generating markup. They make it harder to tell which description is authoritative, and they double the places you have to update when a price, author or date changes.
Finding every block, including hidden ones
View source (Ctrl+U, or Cmd+Option+U on a Mac) and search for application/ld+json to
count the blocks in the HTML your server sends. That misses anything added by JavaScript, so also check
the rendered page. Open developer tools, go to the Console and run:
document.querySelectorAll('script[type="application/ld+json"]').forEach((s, i) => {
try {
const d = JSON.parse(s.textContent);
const nodes = d['@graph'] || (Array.isArray(d) ? d : [d]);
console.log(i, nodes.map(n => n['@type']).join(', '));
} catch (e) { console.log(i, 'unreadable JSON'); }
});
This prints each block's number and the types inside it, whether it came from the server or a script. If the console shows more blocks than view-source did, the extra ones are injected by JavaScript, often from a tag manager or an app.
The Rich Results Test also renders the page and lists detected items by rich result type. Two entries for the same type on a page that should have one is the clearest sign of a clash.
Deciding who owns each type
The durable fix is a simple rule: each type on a page has exactly one source. Decide it once for the whole site, per template, and write it down.
| Type | Usual owner | Role for the generator |
|---|---|---|
| WebSite, WebPage, BreadcrumbList | SEO plugin or theme | None. Remove these from generated output. |
| Organization, author Person | SEO plugin, with fixed @id values | Reference
by @id only. |
| Article, BlogPosting | SEO plugin | Usually none; occasionally extra properties merged in. |
| Product with price and stock | Shop platform or theme, from live data | Enrich with descriptive properties only if the platform omits them. |
| Ratings and reviews | The review app that displays them | None. |
| Recipe, Event, Course, JobPosting, Service | Often nobody | The best use of generated markup. |
When the generator's version of a type is clearly better than your plugin's, you can reverse the ownership: turn the plugin's output for that type off and keep the generated one. Just do not run both.
The fix, step by step
Step 1
Count the JSON-LD blocks on the live page
Open the published page, not the editor preview, and list every structured data block using view source for server-rendered markup and the console snippet for anything added by scripts. Note the number of blocks and the types inside each. Then run the URL through the Rich Results Test and check whether any rich result type is detected twice. Keep this list, because you will compare against it when you finish.
Step 2
Map each block to the system that writes it
For each block, work out its source: the SEO plugin, the theme, the shop platform, a review or events app, a tag manager, or a generated block pasted into the content or a header field. Plugin comments, the position in the HTML and switching a plugin off on a staging copy all help. A block you cannot trace is usually a forgotten paste or a tag manager tag, so check both before moving on.
Step 3
Choose one owner for each type
For every type that appears more than once, pick the source that should keep it. Prefer the one that reads live data, such as the shop platform for price and stock, and the one that already manages site-wide identifiers, such as the SEO plugin for Organization and authors. Choose the generated version only where it is clearly more complete and accurate. Record the decision per template so the next person follows it.
Step 4
Strip the generated block down to what is missing
Edit the generated JSON-LD so it only contains types no other source provides.
Delete generated WebSite, WebPage, BreadcrumbList and Organization nodes, and any Article or Product
the plugin or platform already writes. Where the remaining node needs an author, publisher or parent
page, replace the inline copy with a reference to the existing @id. What is left is usually
a single focused node such as a Recipe or Event.
Step 5
Switch off the losing source
Where you decided the generated markup should own a type, turn that type off in the plugin, theme or app rather than leaving both running. Use the plugin's per-post schema settings, the theme's template file or the app's structured data option. Remove old pasted blocks from post content, header script fields and tag manager tags. Make changes on a staging copy first where you can, since theme edits can remove more than you intend.
Step 6
Retest the rendered page
Clear any page cache, then repeat the count from step 1 on the live page and run the Rich Results Test again. Each rich result type should now appear once, with no conflicting values between blocks. Compare the remaining values with the visible page. Check one more page from each template you changed, because a theme or plugin setting affects every page that uses it, not only the one you tested.
Merging instead of deleting
Sometimes neither source is complete. The plugin's Article has the right author and dates, and the
generated block adds a useful about or mentions list. You can combine them
without creating a second article by giving the generated node the same @id as the plugin's
node. JSON-LD treats nodes with the same identifier as one entity, so the properties are read together.
A cleaner approach, where the plugin allows it, is to add the extra properties through the plugin's
own filter or custom fields so one block contains everything. If you do merge by @id, the
two nodes must not disagree: a property given different values in each becomes a conflict, not a merge.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Recipe",
"@id": "https://example.co.uk/blackberry-apple-crumble/#recipe",
"name": "Blackberry and apple crumble",
"mainEntityOfPage": { "@id": "https://example.co.uk/blackberry-apple-crumble/" },
"author": { "@id": "https://example.co.uk/#/schema/person/1" },
"publisher": { "@id": "https://example.co.uk/#organization" },
"image": ["https://example.co.uk/images/crumble-4x3.jpg"],
"recipeYield": "6 servings",
"prepTime": "PT20M",
"cookTime": "PT40M",
"recipeIngredient": ["500 g cooking apples", "250 g blackberries", "150 g plain flour", "100 g butter", "75 g demerara sugar"],
"recipeInstructions": [
{ "@type": "HowToStep", "text": "Peel, core and slice the apples, then mix with the blackberries in a baking dish." },
{ "@type": "HowToStep", "text": "Rub the butter into the flour, stir in the sugar and scatter over the fruit." },
{ "@type": "HowToStep", "text": "Bake at 190C for 40 minutes until golden." }
]
}
]
}
</script>
This block is what the generator's output looks like after step 4: only the Recipe, with the author,
publisher and page linked to the @id values the SEO plugin already publishes. The identifiers
shown are placeholders; copy the real ones from your own page source.
Platform notes
WordPress
Yoast SEO, Rank Math and All in One SEO each let you change the schema type per post and switch off
article markup for a post type in their settings. Yoast also offers a developer filter, wpseo_json_ld_output,
that turns its structured data off entirely, which is only sensible if something else will provide WebSite,
WebPage and Organization. Page builders and recipe plugins may add their own blocks too, so check them
before assuming the SEO plugin is the only source. Generated blocks pasted in a Custom HTML block are
stored in the post content and survive plugin changes, which is why they are easy to forget.
Shopify
Many themes output Product markup in the product template, often in a section or snippet file, and
review apps commonly add their own ratings markup. Before adding a generated Product, check the theme
code for existing application/ld+json output and the review app's settings for a structured
data option. Keep price and availability with whichever source reads live product data, so sales and
stock changes are reflected automatically.
Custom and headless sites
Search the codebase for application/ld+json. Layout components often emit site-wide markup
that page components then repeat. A single helper that builds one @graph per page, with
each type added in one place, prevents duplicates by design.
1. Is it bad to have more than one JSON-LD script on a page?
No. Several blocks from different sources are normal and Google reads all of them. A page can properly carry breadcrumbs from a plugin, an organisation from the theme and a recipe from a generated block. The problem is two blocks describing the same thing, such as two Articles or two Products for one page, especially when their values differ. Count the types, not the scripts: each thing the page is about should be described once.
2. Will Google penalise my site for duplicate schema?
Google does not treat duplicate markup as a violation in itself, so you should not expect a manual action simply for having two Article blocks. The cost is confusion. Google has to choose between conflicting headlines, authors or prices, may show the less accurate one, and reports warnings against the weaker copy. Clean markup with one description per entity is easier to maintain and removes the risk of the wrong price or rating appearing in search results.
3. Should I remove my SEO plugin's markup or the generated block?
Usually the generated block, or at least the parts of it that repeat what the plugin does. SEO plugins manage site-wide identifiers for your organisation and authors and update dates automatically, while a pasted block is a fixed snapshot. Keep the generated markup for types the plugin does not produce, such as Recipe, Event or Course. Switch the plugin's version off only when the generated one is clearly better and you are prepared to maintain it.
4. Why does the Rich Results Test show two products when I only added one?
Something else on the page is also writing Product markup. On Shopify the theme's product template and review apps are the usual sources; on WooCommerce the shop plugin and an SEO plugin may both output it. Use the console snippet in this guide to list every block with its types, then trace each one to its source. Keep the version that reads live price and stock data and remove or disable the other.
5. Can I combine a generated block with my plugin's markup?
Yes, if they describe the same entity with the same identifier. Give the generated node the @id the plugin already uses and the properties are read together as one entity. This only works when the two nodes do not contradict each other; different values for the same property become a conflict. Where your plugin supports adding properties through its own settings or filters, putting everything in one block is simpler to maintain.
6. The generator keeps adding Organization and WebSite. Should I keep them?
Not if your SEO plugin or theme already outputs them, which is the case on most WordPress sites. Delete those nodes from the generated block and reference the existing organisation by its @id where the remaining markup needs a publisher or brand. Site-wide entities should come from one place so their name, logo and profiles are identical on every page. If nothing else on your site provides them, define them once in your template instead.
7. How do I find schema added by Google Tag Manager?
Tag manager markup is added by JavaScript after the page loads, so it does not appear in view source. Use the browser console snippet in this guide, which lists blocks in the rendered page, or run the Rich Results Test, which renders scripts before reading structured data. If a block appears there but not in the source, open your tag manager container and look for custom HTML tags containing application/ld+json, then remove or update them.
The AI Schema Generator reads your page content with an AI model, writes JSON-LD tailored to the content type and validates it against schema.org and Google rich result criteria, ready for you to fit around your existing markup.
Open AI Schema Generator Fixes โ