All fixes
warning
Rule: structured-data-duplicatesWeight: 2/100

Fix: Duplicate structured data

Having multiple instances of single-entity structured data types—like FAQPage, BreadcrumbList, or Organization—on the same page forces search engines to guess which one is authoritative. When Google's parser encounters two competing FAQPage JSON-LD blocks, it typically drops both from the rich results eligibility pool rather than attempting to merge them. This happens frequently in component-based frameworks like Next.js or Nuxt, where a global layout injects a site-wide schema while a specific route injects its own overlapping schema, or when a reusable <FAQ /> component renders its own <script type="application/ld+json"> tag multiple times on a single page.

The structured-data-duplicates rule catches these conflicts before they ship. While multiple Article or Product schemas are valid on an archive or category page, singleton types must be consolidated into a single JSON-LD block. Duplicate structured data does not crash your application, but it silently revokes your eligibility for rich snippets. Losing rich snippet status can cost a page up to 30% of its organic click-through rate.

How do you detect duplicate structured data?

Run the Indxel CLI against your local build or staging URL. The CLI parses the DOM, extracts all JSON-LD nodes, and flags unique schema types that appear more than once in the document.

npx indxel check http://localhost:3000/pricing
 
# Output
Running SEO infrastructure checks...
 
/pricing
  warn  structured-data-duplicates  Multiple FAQPage schemas detected. Found 2 blocks.
 
✖ 1 warning found. 44/45 pages pass.

The structured-data-duplicates rule carries a severity weight of 2/100. It triggers a warning by default, meaning it will not fail a standard CI run unless you pass the --strict flag.

How do you fix duplicate schema blocks in Next.js?

To resolve this warning, you must aggregate the data into a single schema object before rendering the JSON-LD script tag. The exact implementation depends on your rendering strategy and framework.

Next.js App Router: The Layout vs Page Conflict

The most common cause of duplicate schema in the Next.js App Router is defining a schema in layout.tsx and another in page.tsx. Since layouts wrap pages, both script tags end up in the final DOM.

Bad: Two separate FAQPage blocks injected independently.

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const globalFAQ = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [{
      "@type": "Question",
      "name": "What is your refund policy?",
      "acceptedAnswer": { "@type": "Answer", "text": "We offer a 30-day money-back guarantee." }
    }]
  };
 
  return (
    <html lang="en">
      <head>
        <script 
          type="application/ld+json" 
          dangerouslySetInnerHTML={{ __html: JSON.stringify(globalFAQ) }} 
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
 
// app/pricing/page.tsx
export default function PricingPage() {
  const pricingFAQ = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [{
      "@type": "Question",
      "name": "Do you offer enterprise pricing?",
      "acceptedAnswer": { "@type": "Answer", "text": "Yes, contact sales for custom plans." }
    }]
  };
 
  return (
    <main>
      <h1>Pricing</h1>
      <script 
        type="application/ld+json" 
        dangerouslySetInnerHTML={{ __html: JSON.stringify(pricingFAQ) }} 
      />
    </main>
  );
}

Good: Remove the singleton schema from the global layout. Instead, construct a single schema object at the page level that combines all necessary entities.

// app/pricing/page.tsx
export default function PricingPage() {
  // Merge all questions into a single mainEntity array
  const unifiedFAQ = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "What is your refund policy?",
        "acceptedAnswer": { "@type": "Answer", "text": "We offer a 30-day money-back guarantee." }
      },
      {
        "@type": "Question",
        "name": "Do you offer enterprise pricing?",
        "acceptedAnswer": { "@type": "Answer", "text": "Yes, contact sales for custom plans." }
      }
    ]
  };
 
  return (
    <main>
      <h1>Pricing</h1>
      <script 
        type="application/ld+json" 
        dangerouslySetInnerHTML={{ __html: JSON.stringify(unifiedFAQ) }} 
      />
    </main>
  );
}

Next.js Pages Router: Component-Level Injection

If you render a reusable <FAQAccordion /> component multiple times on a page, and that component injects its own schema, you will generate duplicates.

Bad: Component injects schema directly on every instantiation.

// components/FAQAccordion.tsx
export function FAQAccordion({ question, answer }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [{
      "@type": "Question",
      "name": question,
      "acceptedAnswer": { "@type": "Answer", "text": answer }
    }]
  };
 
  return (
    <div>
      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />
      <details>
        <summary>{question}</summary>
        <p>{answer}</p>
      </details>
    </div>
  );
}

Good: Extract the schema generation logic out of the UI component. Pass the data to a dedicated SEO component or a page-level wrapper that aggregates the array.

// pages/pricing.tsx
import Head from 'next/head';
import { FAQAccordion } from '../components/FAQAccordion';
 
const faqs = [
  { q: "What is your refund policy?", a: "We offer a 30-day money-back guarantee." },
  { q: "Do you offer enterprise pricing?", a: "Yes, contact sales for custom plans." }
];
 
export default function PricingPage() {
  const schema = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": faqs.map(faq => ({
      "@type": "Question",
      "name": faq.q,
      "acceptedAnswer": { "@type": "Answer", "text": faq.a }
    }))
  };
 
  return (
    <>
      <Head>
        <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />
      </Head>
      <main>
        {faqs.map((faq, i) => (
          <FAQAccordion key={i} question={faq.q} answer={faq.a} />
        ))}
      </main>
    </>
  );
}

The Indxel SDK Approach

If you use the Indxel SDK, the createMetadata utility handles schema merging automatically. When you define multiple schemas of the same type, the SDK aggregates them into an array or a @graph object depending on the specification.

import { createMetadata } from '@indxel/core';
 
export const metadata = createMetadata({
  structuredData: {
    faq: [
      { question: "What is your refund policy?", answer: "We offer a 30-day money-back guarantee." },
      { question: "Do you offer enterprise pricing?", answer: "Yes, contact sales for custom plans." }
    ]
  }
});

How do you prevent duplicate schema regressions in CI?

Catch duplicate schema configurations before they reach production by running the Indxel CLI in your continuous integration pipeline. Use the --strict flag to fail the build if any warnings, including structured-data-duplicates, are detected.

# .github/workflows/seo-checks.yml
name: SEO CI
on: [push, pull_request]
 
jobs:
  validate-seo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build application
        run: npm run build
        
      - name: Run Indxel checks
        # The --strict flag upgrades warnings (like duplicate schema) to errors
        # The --ci flag formats output for GitHub Actions annotations
        run: npx indxel check --ci --strict

If you only want to fail the build for critical errors but still want visibility into warnings, drop the --strict flag. The CLI will exit with code 0 but still annotate your pull request with the structured-data-duplicates warning.

What are common edge cases when merging JSON-LD?

Developers often introduce validation errors while attempting to fix duplication warnings. Merging JSON-LD is not always a simple array concatenation.

Edge Case 1: The @graph Array Pattern

When a page requires multiple distinct schema types (e.g., WebSite, Organization, and BreadcrumbList), placing them in separate <script> tags is valid but inefficient. Wrapping them in a single @graph array is the cleanest approach. However, if you include two FAQPage objects inside a single @graph, Google will still flag it as a duplicate entity.

// BAD: Two FAQPage entities inside a @graph
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "FAQPage",
      "mainEntity": [{ "@type": "Question", "name": "Q1", "acceptedAnswer": { "@type": "Answer", "text": "A1" } }]
    },
    {
      "@type": "FAQPage",
      "mainEntity": [{ "@type": "Question", "name": "Q2", "acceptedAnswer": { "@type": "Answer", "text": "A2" } }]
    }
  ]
}

Edge Case 2: Third-Party Script Injection

Third-party widgets—like customer review platforms (Trustpilot, Yotpo) or local business map integrations—often inject their own JSON-LD directly into the DOM client-side. If your server-side rendered code includes an Organization schema, and a third-party script injects a second Organization schema on the client, you have a duplicate.

To fix this, you must either disable the schema injection feature in the third-party widget's settings, or remove your server-side schema and pass your entity data to the third-party script to handle the merge.

Edge Case 3: Microdata and JSON-LD Collisions

Mixing JSON-LD (in <script> tags) with Microdata (using itemprop and itemtype HTML attributes) for the same entity type creates duplicates in the parsed structured data tree. Google recommends JSON-LD. Strip all itemscope and itemtype attributes from your HTML templates and rely exclusively on JSON-LD.

Related rules

  • missing-structured-data: Flags pages that lack baseline schema entities (like WebPage or Article).
  • invalid-structured-data: Validates your JSON-LD against schema.org specifications to catch missing required properties (e.g., a Question missing an acceptedAnswer).

Frequently asked questions

Which schema types must be unique per page?

FAQPage, BreadcrumbList, WebSite, Organization, HowTo, LocalBusiness, and SearchAction should only appear once per document. Search engines treat these as page-level or domain-level definitions. Multiple instances create conflicts because a single URL cannot logically represent two different corporate organizations or two distinct primary breadcrumb trails.

Can I have multiple Product or Article schemas on one page?

Yes, multiple Product, Article, Recipe, or Event blocks on the same page are perfectly valid. This is the correct implementation for category pages, blog archive pages, or event listing pages. The structured-data-duplicates rule ignores these entity types entirely.

Does Google merge duplicate schemas automatically?

No, Google does not merge duplicate structured data blocks. If you provide two FAQPage scripts, Google's parser will not concatenate the mainEntity arrays. It will typically invalidate both blocks, resulting in a total loss of rich snippet eligibility for that URL.

How do I combine an Organization schema with a WebSite schema?

Use the @graph property to combine different top-level entity types into a single JSON-LD block. This reduces DOM size and clearly maps the relationships between entities using the @id property.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "WebSite",
      "@id": "https://indxel.com/#website",
      "url": "https://indxel.com",
      "publisher": { "@id": "https://indxel.com/#organization" }
    },
    {
      "@type": "Organization",
      "@id": "https://indxel.com/#organization",
      "name": "Indxel"
    }
  ]
}

Frequently asked questions

Which schema types should be unique per page?

FAQPage, BreadcrumbList, WebSite, Organization, HowTo, LocalBusiness, and SearchAction should only appear once per page. Multiple Article or Product blocks on the same page are fine.

Catch this before it ships

$npx indxel check --ci
Get startedBrowse all fixes
Indxel

SEO validation that runs in your terminal and blocks bad deploys.

GitHubnpm

Product

  • Documentation
  • Pricing
  • Plus Plan
  • CI/CD Guard
  • Indexation
  • Free Tools
  • Blog

Comparisons

  • vs Semrush
  • vs Ahrefs
  • vs Moz
  • vs Screaming Frog
  • All comparisons

Integrations

  • Vercel
  • GitHub Actions
  • Netlify
  • Docker
  • All integrations

Resources

  • Frameworks & use cases
  • Next.js
  • For freelancers
  • For agencies
  • SEO Glossary

Built with care. MIT Licensed.

PrivacyTermsLegalContact