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.
Critical because missing or invalid AggregateRating structured data eliminates star-rating rich snippets from Google SERPs, directly reducing organic click-through rate.
Inject a <script type="application/ld+json"> block with dynamically computed values in components/ProductPage.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.
ID: ecommerce-reviews.schema-seo.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:
// 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:
export const metadata = {
other: {
'ld+json': JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
// ...
})
}
}