# aggregateRating in structured data matches visible on-page values

- **Pattern:** `ab-001020` (`directory-listing-schema.structured-data.rating-accuracy`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001020
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001020)

## Why it matters

A JSON-LD `aggregateRating` block that shows 4.5 stars while the page displays 4.3 stars is an active schema-org policy violation — Google's Rich Results guidelines prohibit structured data that misrepresents visible content. Discovery of the discrepancy triggers manual review and can result in a site-wide rich-result penalty. Beyond compliance, rating mismatch is a data-integrity failure: users who click a listing based on an inflated SERP star count and see a lower on-page rating experience an immediate trust collapse. The `ratingValue` and `reviewCount` in schema-org AggregateRating must be computed from the same database query that drives the visible UI.

## Severity rationale

High because mismatched structured-data ratings violate Google's explicit rich-results content policies, risking manual review penalties that suppress all rich results site-wide.

## Remediation

Derive the JSON-LD `aggregateRating` from the same source as the rendered rating — never hardcode or cache independently. Compute both from a single database read per listing.

```ts
// In your listing data fetcher
const listing = await db.listing.findUnique({
  where: { id },
  select: {
    name: true,
    averageRating: true,
    reviewCount: true,
    // ...
  },
});

const jsonLd = {
  "@type": "LocalBusiness",
  ...(listing.reviewCount > 0 && {
    "aggregateRating": {
      "@type": "AggregateRating",
      "ratingValue": listing.averageRating,
      "reviewCount": listing.reviewCount,
    },
  }),
};
```

The guard `listing.reviewCount > 0` prevents an empty `aggregateRating` block, which is separately flagged as invalid by Google's structured data validator.

## Detection

- **ID:** `rating-accuracy`
- **Severity:** `high`
- **What to look for:** Count all listings with ratings. For each, find the aggregateRating object in JSON-LD. Compare its `ratingValue` and `reviewCount` against the ratings/review counts displayed on the page. Verify they match.
- **Pass criteria:** The JSON-LD aggregateRating values match the displayed rating and review count on the page — 100% of displayed ratings must match the computed average of individual reviews, with no more than 0.1 point deviation. Report: "X rated listings found, all Y display accurate aggregate ratings."
- **Fail criteria:** The JSON-LD shows one rating/count while the page displays different values, or aggregateRating is present but empty.
- **Skip (N/A) when:** The listing has no ratings or reviews.
- **Detail on fail:** Quote the actual rating values showing the discrepancy. Example: `"JSON-LD shows 4.5 rating and 120 reviews, but page displays 4.3 stars and 95 reviews"` or `"aggregateRating block present but empty"`
- **Remediation:** Populate aggregateRating from the same source as the displayed ratings:

  ```ts
  const jsonLd = {
    "@type": "LocalBusiness",
    "aggregateRating": {
      "@type": "AggregateRating",
      "ratingValue": listing.averageRating,
      "reviewCount": listing.reviewCount
    }
  }
  ```

## External references

- schema-org AggregateRating
- schema-org ratingValue
- schema-org reviewCount

Taxons: findability, data-integrity

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