# Schema.org AggregateRating JSON-LD markup is present and valid

- **Pattern:** `ab-001216` (`ecommerce-reviews.schema-seo.aggregate-rating-schema`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001216
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001216)

## Why it matters

Google requires a valid schema.org `AggregateRating` block to render star ratings in search results (rich snippets). Without it, your product pages compete in SERPs as plain blue links against competitors whose pages show 4.8★ inline — a significant CTR disadvantage. The schema.org `Product.aggregateRating` mapping specifies `ratingValue` must be a number, not a string: submitting `"4.5"` instead of `4.5` causes Google's Rich Results Test to flag the markup as invalid and suppresses the snippet. Hardcoded values are doubly penalised — they diverge from the displayed aggregate over time, which Google can detect by comparing the structured data to the visible page content.

## Severity rationale

Critical because missing or invalid AggregateRating structured data eliminates star-rating rich snippets from Google SERPs, directly reducing organic click-through rate.

## Remediation

Inject a `<script type="application/ld+json">` block with dynamically computed values in `components/ProductPage.tsx`.

```tsx
// components/ProductPage.tsx
export async function ProductPage({ product }: { product: Product }) {
  const approved = product.reviews.filter(r => r.status === 'approved')
  const ratingValue = approved.length > 0
    ? approved.reduce((s, r) => s + r.rating, 0) / approved.length
    : undefined

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    ...(ratingValue !== undefined && {
      aggregateRating: {
        '@type': 'AggregateRating',
        ratingValue: parseFloat(ratingValue.toFixed(1)), // number, not string
        reviewCount: approved.length,
        bestRating: 5,
        worstRating: 1
      }
    })
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
      />
      {/* page content */}
    </>
  )
}
```

Omit the `aggregateRating` block entirely when `approved.length === 0` — Google rejects a `reviewCount: 0` block as invalid.

## Detection

- **ID:** `aggregate-rating-schema`
- **Severity:** `critical`
- **What to look for:** Before evaluating, quote the JSON-LD block from product page components. Count the number of required AggregateRating fields present: (1) `@type: "AggregateRating"`, (2) `ratingValue` as a number, (3) `reviewCount` as an integer, (4) `bestRating`, (5) `worstRating`. Report: X of 5 fields present.
- **Pass criteria:** Product pages include a `<script type="application/ld+json">` block with a valid AggregateRating schema containing at least 3 of 5 required fields: `ratingValue` (number between 0-5), `reviewCount` (integer), and `@type: "AggregateRating"`. Values must be dynamically generated from review data, not hardcoded.
- **Fail criteria:** No AggregateRating JSON-LD block found on any product page component, or the block exists but has fewer than 3 of 5 required fields, or `ratingValue` is a string instead of a number. Do not pass when a schema block exists but uses hardcoded rating values.
- **Skip (N/A) when:** The project has no product pages or no reviews system (search for `application/ld+json` across all page components).
- **Detail on fail:** `"No JSON-LD script tag found in any of 4 product page components. 0 of 5 AggregateRating fields present."` or `"Schema block found but ratingValue is string '4.5' (should be number 4.5). 2 of 5 fields valid."`
- **Cross-reference:** Related to `ecommerce-reviews.display-ux.aggregate-rating-visible` (schema values must match displayed aggregate) and `ecommerce-reviews.schema-seo.review-schema` (individual review schema complements aggregate).
- **Remediation:** Add AggregateRating schema to product pages in `components/ProductPage.tsx`:

  ```tsx
  // components/ProductPage.tsx
  export function ProductPage({ product }) {
    const schema = {
      '@context': 'https://schema.org',
      '@type': 'Product',
      name: product.name,
      aggregateRating: {
        '@type': 'AggregateRating',
        ratingValue: product.avgRating,
        reviewCount: product.reviewCount,
        bestRating: 5,
        worstRating: 1
      }
    }

    return (
      <>
        <script type="application/ld+json">
          {JSON.stringify(schema)}
        </script>
        <h1>{product.name}</h1>
        {/* ... */}
      </>
    )
  }
  ```

  Or use Next.js metadata:

  ```ts
  export const metadata = {
    other: {
      'ld+json': JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Product',
        // ...
      })
    }
  }
  ```

## External references

- schema-org AggregateRating
- schema-org Product.aggregateRating

Taxons: findability

HTML version: https://auditbuffet.com/patterns/ab-001216
