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:
- Declare both products with their specs and ratings
- Surface aggregate review data from multiple sources
- Show price ranges that update automatically
- 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";
};
};
}
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 */}
</>
);
}
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,
},
})),
};
}
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:
-
Google Rich Results Test —
https://search.google.com/test/rich-results -
Schema.org Validator —
https://validator.schema.org - 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();
}
});
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" }
]
}
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:
- Start with
ItemList+Productfor your two entities - Add
FAQPagefor your comparison questions - Add
BreadcrumbListfor navigation context - Validate everything before deploying
- 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)