DEV Community

Daniel Rozin
Daniel Rozin

Posted on • Originally published at aversusb.net

JSON-LD for Product Comparisons: How to Win Rich Snippets in Google

If you build product comparison pages and you're not using JSON-LD, you're leaving traffic on the table.

Rich snippets — star ratings, price ranges, review counts displayed directly in Google results — can boost click-through rates by 30% or more. And for comparison pages, structured data is especially powerful because it tells Google exactly what two products are being compared and how.

At SmartReview, structured data is a core part of every comparison page we generate. Here's exactly how we implement it.

Why JSON-LD for Comparisons?

Google supports several structured data formats, but JSON-LD is the recommended approach. It's:

  • Non-invasive — lives in a <script> tag, doesn't clutter your HTML
  • Easy to template — generate it from your data model
  • Well-documented — Schema.org has clear specs for Product, Review, and AggregateRating

For comparison pages specifically, JSON-LD lets you:

  1. Declare both products with their specs and ratings
  2. Surface aggregate review data from multiple sources
  3. Show price ranges that update automatically
  4. Appear in Google's product comparison carousels

The Schema Structure

A comparison page should output an ItemList containing two Product entities. Here's the full structure:

interface ComparisonJsonLd {
  "@context": "https://schema.org";
  "@type": "ItemList";
  name: string;           // e.g. "AirPods Pro vs Sony WF-1000XM5"
  description: string;
  numberOfItems: 2;
  itemListElement: ProductJsonLd[];
}

interface ProductJsonLd {
  "@type": "ListItem";
  position: number;
  item: {
    "@type": "Product";
    name: string;
    brand: { "@type": "Brand"; name: string };
    image: string;
    description: string;
    aggregateRating?: {
      "@type": "AggregateRating";
      ratingValue: string;
      bestRating: "5";
      worstRating: "1";
      reviewCount: string;
    };
    offers?: {
      "@type": "AggregateOffer";
      lowPrice: string;
      highPrice: string;
      priceCurrency: "USD";
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

Implementation in Next.js

Here's how we render this in a Next.js comparison page:

// app/compare/[slug]/page.tsx
import { Metadata } from "next";

interface ComparisonPageProps {
  params: { slug: string };
}

function buildJsonLd(comparison: ComparisonData) {
  return {
    "@context": "https://schema.org",
    "@type": "ItemList",
    name: `${comparison.entityA.name} vs ${comparison.entityB.name}`,
    description: comparison.shortAnswer,
    numberOfItems: 2,
    itemListElement: [
      comparison.entityA,
      comparison.entityB,
    ].map((entity, i) => ({
      "@type": "ListItem",
      position: i + 1,
      item: {
        "@type": "Product",
        name: entity.name,
        brand: {
          "@type": "Brand",
          name: entity.brand,
        },
        image: entity.imageUrl,
        description: entity.description,
        ...(entity.rating && {
          aggregateRating: {
            "@type": "AggregateRating",
            ratingValue: entity.rating.toFixed(1),
            bestRating: "5",
            worstRating: "1",
            reviewCount: String(entity.reviewCount),
          },
        }),
        ...(entity.price && {
          offers: {
            "@type": "AggregateOffer",
            lowPrice: entity.price.low.toFixed(2),
            highPrice: entity.price.high.toFixed(2),
            priceCurrency: "USD",
          },
        }),
      },
    })),
  };
}

export default function ComparisonPage({ params }: ComparisonPageProps) {
  const comparison = getComparison(params.slug);
  const jsonLd = buildJsonLd(comparison);

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* rest of your comparison page */}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adding FAQ Schema

Comparison pages naturally generate FAQ content — "Is X better than Y for gaming?" "Which is cheaper?" etc. Adding FAQPage schema captures People Also Ask boxes:

function buildFaqJsonLd(faqs: { question: string; answer: string }[]) {
  return {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    mainEntity: faqs.map((faq) => ({
      "@type": "Question",
      name: faq.question,
      acceptedAnswer: {
        "@type": "Answer",
        text: faq.answer,
      },
    })),
  };
}
Enter fullscreen mode Exit fullscreen mode

We typically include 5-8 FAQs per comparison page, targeting the actual "People Also Ask" queries we find in SERPs via our keyword discovery pipeline.

Validating Your Structured Data

Before deploying, always validate with:

  1. Google Rich Results Testhttps://search.google.com/test/rich-results
  2. Schema.org Validatorhttps://validator.schema.org
  3. Automated testing — we run a Playwright test that checks every comparison page:
// tests/structured-data.spec.ts
import { test, expect } from "@playwright/test";

test("comparison page has valid JSON-LD", async ({ page }) => {
  await page.goto("/compare/airpods-pro-vs-sony-wf1000xm5");

  const jsonLd = await page.evaluate(() => {
    const script = document.querySelector(
      'script[type="application/ld+json"\n    );
    return script ? JSON.parse(script.textContent || "{}") : null;
  });

  expect(jsonLd).not.toBeNull();
  expect(jsonLd["@type"]).toBe("ItemList");
  expect(jsonLd.itemListElement).toHaveLength(2);

  for (const item of jsonLd.itemListElement) {
    expect(item.item["@type"]).toBe("Product");
    expect(item.item.name).toBeTruthy();
    expect(item.item.brand.name).toBeTruthy();
  }
});
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

After building 10,000+ comparison pages, here are the structured data mistakes we see most:

1. Missing aggregateRating sources

Google wants to see that your ratings come from real reviews. If you're aggregating from Amazon, Reddit, and RTINGS, document your methodology. We include a reviewCount that reflects actual reviews processed.

2. Stale prices

Don't hardcode prices. Use AggregateOffer with lowPrice/highPrice ranges that update from your data pipeline. A price that's wrong erodes trust with both users and Google.

3. Over-optimized FAQ content

Your FAQ answers should be genuinely helpful, not keyword-stuffed. Google's helpful content update actively demotes pages with thin, repetitive FAQ sections.

4. No canonical URL

If your comparison exists at both /compare/a-vs-b and /compare/b-vs-a, set a canonical. We normalize to alphabetical order.

5. Forgetting BreadcrumbList

Breadcrumbs help Google understand your site hierarchy:

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://aversusb.net" },
    { "@type": "ListItem", "position": 2, "name": "Earbuds", "item": "https://aversusb.net/category/earbuds" },
    { "@type": "ListItem", "position": 3, "name": "AirPods Pro vs Sony WF-1000XM5" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Results: What Structured Data Actually Does

After implementing comprehensive JSON-LD across our comparison pages:

  • CTR increased 34% on pages with rich snippets vs. those without
  • Average position improved 1.2 spots (structured data correlates with quality signals)
  • FAQ schema captured 40+ People Also Ask placements in our first month
  • Price display in SERPs drove 2x more affiliate clicks

The compound effect is real: better CTR → better engagement signals → higher rankings → more traffic → more engagement. It's a virtuous cycle.

Try It Yourself

If you're building comparison content:

  1. Start with ItemList + Product for your two entities
  2. Add FAQPage for your comparison questions
  3. Add BreadcrumbList for navigation context
  4. Validate everything before deploying
  5. Monitor in Google Search Console under "Enhancements"

Check out live examples on aversusb.net — view source on any comparison page to see the full JSON-LD implementation.


Part 4 of our "Building SmartReview" series. Previous: Part 3: The Hidden SEO Goldmine

Top comments (0)