{"slug":"json-ld-product-comparisons","title":"JSON-LD for Product Comparisons: How to Win Rich Snippets in Google","excerpt":"If you build product comparison pages and you're not using JSON-LD, you're leaving traffic on the...","content":"If you build product comparison pages and you're not using JSON-LD, you're leaving traffic on the table.\n\nRich snippets u2014 star ratings, price ranges, review counts displayed directly in Google results u2014 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.\n\nAt [SmartReview](https://www.aversusb.net), structured data is a core part of every comparison page we generate. Here's exactly how we implement it.\n\n## Why JSON-LD for Comparisons?\n\nGoogle supports several structured data formats, but JSON-LD is the recommended approach. It's:\n\n- **Non-invasive** u2014 lives in a `<script>` tag, doesn't clutter your HTML\n- **Easy to template** u2014 generate it from your data model\n- **Well-documented** u2014 Schema.org has clear specs for Product, Review, and AggregateRating\n\nFor comparison pages specifically, JSON-LD lets you:\n\n1. Declare both products with their specs and ratings\n2. Surface aggregate review data from multiple sources\n3. Show price ranges that update automatically\n4. Appear in Google's product comparison carousels\n\n## The Schema Structure\n\nA comparison page should output an `ItemList` containing two `Product` entities. Here's the full structure:\n\n```typescript\ninterface ComparisonJsonLd {\n  \"@context\": \"https://schema.org\";\n  \"@type\": \"ItemList\";\n  name: string;           // e.g. \"AirPods Pro vs Sony WF-1000XM5\"\n  description: string;\n  numberOfItems: 2;\n  itemListElement: ProductJsonLd[];\n}\n\ninterface ProductJsonLd {\n  \"@type\": \"ListItem\";\n  position: number;\n  item: {\n    \"@type\": \"Product\";\n    name: string;\n    brand: { \"@type\": \"Brand\"; name: string };\n    image: string;\n    description: string;\n    aggregateRating?: {\n      \"@type\": \"AggregateRating\";\n      ratingValue: string;\n      bestRating: \"5\";\n      worstRating: \"1\";\n      reviewCount: string;\n    };\n    offers?: {\n      \"@type\": \"AggregateOffer\";\n      lowPrice: string;\n      highPrice: string;\n      priceCurrency: \"USD\";\n    };\n  };\n}\n```\n\n## Implementation in Next.js\n\nHere's how we render this in a Next.js comparison page:\n\n```tsx\n// app/compare/[slug]/page.tsx\nimport { Metadata } from \"next\";\n\ninterface ComparisonPageProps {\n  params: { slug: string };\n}\n\nfunction buildJsonLd(comparison: ComparisonData) {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"ItemList\",\n    name: `${comparison.entityA.name} vs ${comparison.entityB.name}`,\n    description: comparison.shortAnswer,\n    numberOfItems: 2,\n    itemListElement: [\n      comparison.entityA,\n      comparison.entityB,\n    ].map((entity, i) => ({\n      \"@type\": \"ListItem\",\n      position: i + 1,\n      item: {\n        \"@type\": \"Product\",\n        name: entity.name,\n        brand: {\n          \"@type\": \"Brand\",\n          name: entity.brand,\n        },\n        image: entity.imageUrl,\n        description: entity.description,\n        ...(entity.rating && {\n          aggregateRating: {\n            \"@type\": \"AggregateRating\",\n            ratingValue: entity.rating.toFixed(1),\n            bestRating: \"5\",\n            worstRating: \"1\",\n            reviewCount: String(entity.reviewCount),\n          },\n        }),\n        ...(entity.price && {\n          offers: {\n            \"@type\": \"AggregateOffer\",\n            lowPrice: entity.price.low.toFixed(2),\n            highPrice: entity.price.high.toFixed(2),\n            priceCurrency: \"USD\",\n          },\n        }),\n      },\n    })),\n  };\n}\n\nexport default function ComparisonPage({ params }: ComparisonPageProps) {\n  const comparison = getComparison(params.slug);\n  const jsonLd = buildJsonLd(comparison);\n\n  return (\n    <>\n      <script\n        type=\"application/ld+json\"\n        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n      />\n      {/* rest of your comparison page */}\n    </>\n  );\n}\n```\n\n## Adding FAQ Schema\n\nComparison pages naturally generate FAQ content u2014 \"Is X better than Y for gaming?\" \"Which is cheaper?\" etc. Adding `FAQPage` schema captures People Also Ask boxes:\n\n```typescript\nfunction buildFaqJsonLd(faqs: { question: string; answer: string }[]) {\n  return {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"FAQPage\",\n    mainEntity: faqs.map((faq) => ({\n      \"@type\": \"Question\",\n      name: faq.question,\n      acceptedAnswer: {\n        \"@type\": \"Answer\",\n        text: faq.answer,\n      },\n    })),\n  };\n}\n```\n\nWe typically include 5-8 FAQs per comparison page, targeting the actual \"People Also Ask\" queries we find in SERPs via our keyword discovery pipeline.\n\n## Validating Your Structured Data\n\nBefore deploying, always validate with:\n\n1. **Google Rich Results Test** u2014 `https://search.google.com/test/rich-results`\n2. **Schema.org Validator** u2014 `https://validator.schema.org`\n3. **Automated testing** u2014 we run a Playwright test that checks every comparison page:\n\n```typescript\n// tests/structured-data.spec.ts\nimport { test, expect } from \"@playwright/test\";\n\ntest(\"comparison page has valid JSON-LD\", async ({ page }) => {\n  await page.goto(\"/compare/airpods-pro-vs-sony-wf1000xm5\");\n  \n  const jsonLd = await page.evaluate(() => {\n    const script = document.querySelector(\n      'script[type=\"application/ld+json\"\n    );\n    return script ? JSON.parse(script.textContent || \"{}\") : null;\n  });\n\n  expect(jsonLd).not.toBeNull();\n  expect(jsonLd[\"@type\"]).toBe(\"ItemList\");\n  expect(jsonLd.itemListElement).toHaveLength(2);\n  \n  for (const item of jsonLd.itemListElement) {\n    expect(item.item[\"@type\"]).toBe(\"Product\");\n    expect(item.item.name).toBeTruthy();\n    expect(item.item.brand.name).toBeTruthy();\n  }\n});\n```\n\n## Common Mistakes\n\nAfter building 10,000+ comparison pages, here are the structured data mistakes we see most:\n\n### 1. Missing `aggregateRating` sources\nGoogle 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.\n\n### 2. Stale prices\nDon'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.\n\n### 3. Over-optimized FAQ content\nYour FAQ answers should be genuinely helpful, not keyword-stuffed. Google's helpful content update actively demotes pages with thin, repetitive FAQ sections.\n\n### 4. No `canonical` URL\nIf your comparison exists at both `/compare/a-vs-b` and `/compare/b-vs-a`, set a canonical. We normalize to alphabetical order.\n\n### 5. Forgetting `BreadcrumbList`\nBreadcrumbs help Google understand your site hierarchy:\n\n```json\n{\n  \"@context\": \"https://schema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https://aversusb.net\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Earbuds\", \"item\": \"https://aversusb.net/category/earbuds\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"AirPods Pro vs Sony WF-1000XM5\" }\n  ]\n}\n```\n\n## Results: What Structured Data Actually Does\n\nAfter implementing comprehensive JSON-LD across our comparison pages:\n\n- **CTR increased 34%** on pages with rich snippets vs. those without\n- **Average position improved 1.2 spots** (structured data correlates with quality signals)\n- **FAQ schema captured 40+ People Also Ask placements** in our first month\n- **Price display in SERPs** drove 2x more affiliate clicks\n\nThe compound effect is real: better CTR u2192 better engagement signals u2192 higher rankings u2192 more traffic u2192 more engagement. It's a virtuous cycle.\n\n## Try It Yourself\n\nIf you're building comparison content:\n\n1. Start with `ItemList` + `Product` for your two entities\n2. Add `FAQPage` for your comparison questions\n3. Add `BreadcrumbList` for navigation context\n4. Validate everything before deploying\n5. Monitor in Google Search Console under \"Enhancements\"\n\nCheck out live examples on [aversusb.net](https://www.aversusb.net) u2014 view source on any comparison page to see the full JSON-LD implementation.\n\n---\n\n*Part 4 of our \"Building SmartReview\" series. Previous: [Part 3: The Hidden SEO Goldmine](https://dev.to/danie_rozin/the-hidden-seo-goldmine-why-x-vs-y-keywords-convert-3x-better-5bmo)*","category":"business","tags":["seo","webdev","javascript","tutorial"],"url":"https://www.aversusb.net/blog/json-ld-product-comparisons","publishedAt":"2026-06-25T11:21:35.261Z","updatedAt":"2026-06-25T11:21:35.262Z","articleSchema":{"@context":"https://schema.org","@type":"BlogPosting","@id":"https://www.aversusb.net/blog/json-ld-product-comparisons#article","headline":"JSON-LD for Product Comparisons: How to Win Rich Snippets in Google","description":"If you build product comparison pages and you're not using JSON-LD, you're leaving traffic on the...","abstract":"If you build product comparison pages and you're not using JSON-LD, you're leaving traffic on the...","url":"https://www.aversusb.net/blog/json-ld-product-comparisons","image":{"@type":"ImageObject","@id":"https://www.aversusb.net/blog/json-ld-product-comparisons#primaryImage","url":"https://www.aversusb.net/api/og?title=JSON-LD%20for%20Product%20Comparisons%3A%20How%20to%20Win%20Rich%20Snippets%20in%20Google&type=blog","contentUrl":"https://www.aversusb.net/api/og?title=JSON-LD%20for%20Product%20Comparisons%3A%20How%20to%20Win%20Rich%20Snippets%20in%20Google&type=blog","width":1200,"height":630,"caption":"JSON-LD for Product Comparisons: How to Win Rich Snippets in Google"},"thumbnailUrl":"https://www.aversusb.net/api/og?title=JSON-LD%20for%20Product%20Comparisons%3A%20How%20to%20Win%20Rich%20Snippets%20in%20Google&type=blog","contentReferenceTime":"2026-06-25T11:21:35.262Z","datePublished":"2026-06-25T11:21:35.261Z","dateCreated":"2026-06-25T11:21:35.261Z","dateModified":"2026-06-25T11:21:35.262Z","author":{"@type":"Organization","@id":"https://www.aversusb.net/#organization","name":"A Versus B"},"publisher":{"@type":"Organization","@id":"https://www.aversusb.net/#organization","name":"A Versus B"},"inLanguage":"en-US","isPartOf":{"@type":"WebSite","@id":"https://www.aversusb.net/#website"},"keywords":"seo, webdev, javascript, tutorial","articleSection":"business","wordCount":955,"license":"https://creativecommons.org/licenses/by/4.0/","speakable":{"@type":"SpeakableSpecification","cssSelector":["h1",".article-excerpt",".article-intro","#article-summary"]},"accessMode":["textual"],"accessModeSufficient":[{"@type":"ItemList","itemListElement":["textual"]}],"isAccessibleForFree":true}}