An aggregate rating is the single highest-impact trust signal on a product page — multiple e-commerce studies put conversion lift from visible star ratings at 15–40%. Missing or hardcoded aggregates also break schema.org AggregateRating structured data (see ecommerce-reviews.schema-seo.aggregate-rating-schema), eliminating the review-rich snippet in Google SERPs. Beyond conversion, a hardcoded value is a dark-pattern that regulators including the FTC treat as deceptive when it doesn't reflect real buyer sentiment. The aggregate must be dynamically computed from the approved reviews table to be both accurate and legally defensible.
Critical because an absent or hardcoded aggregate rating directly suppresses conversion, removes Google rich-snippet eligibility, and constitutes a deceptive practice if the displayed score doesn't match real data.
Compute the aggregate from approved reviews in components/ProductHeader.tsx and display at least average rating, visual stars, and review count above the fold.
// components/ProductHeader.tsx
export async function ProductHeader({ productId }: { productId: string }) {
const reviews = await db.reviews.findMany({
where: { product_id: productId, status: 'approved' },
select: { rating: true }
})
const count = reviews.length
const avg = count > 0
? (reviews.reduce((s, r) => s + r.rating, 0) / count).toFixed(1)
: null
return (
<div className="product-header">
{avg && (
<div className="rating-summary" aria-label={`${avg} out of 5, ${count} reviews`}>
<StarDisplay rating={Number(avg)} />
<span>{avg} ({count} {count === 1 ? 'review' : 'reviews'})</span>
</div>
)}
</div>
)
}
The avg value must flow into the schema.org AggregateRating block on the same page to keep structured data and displayed text in sync.
ID: ecommerce-reviews.display-ux.aggregate-rating-visible
Severity: critical
What to look for: Before evaluating, quote the product page component that renders the aggregate rating. Count the number of aggregate rating data points displayed: (1) average star rating as a number, (2) visual star representation, (3) total review count, (4) rating distribution breakdown (e.g., "5-star: 40%"). Report: X of 4 data points present.
Pass criteria: Product pages display at least 3 of 4 aggregate rating data points (average star rating as a number, visual star representation, and total review count) in a location visible without scrolling on desktop viewports. The aggregate is dynamically computed from the reviews table, not hardcoded.
Fail criteria: Fewer than 3 of 4 aggregate rating data points are displayed, the aggregate is hidden behind a click or accordion, or the values are hardcoded rather than dynamically calculated from actual review data. Do not pass when a static "4.5 stars" appears in markup without any database query or computation.
Skip (N/A) when: The project has no product pages or no review system (search for product detail components and review model/schema).
Detail on fail: "1 of 4 aggregate rating data points present (review count only). No average star value or visual stars displayed." or "Aggregate rating hardcoded as '4.8 (200 reviews)' in JSX — no dynamic calculation from reviews table."
Cross-reference: Related to ecommerce-reviews.schema-seo.aggregate-rating-schema (schema.org must match displayed aggregate) and ecommerce-reviews.display-ux.review-display-complete (individual reviews feed the aggregate).
Remediation: Add an aggregate rating component to your product page in components/ProductHeader.tsx:
// components/ProductHeader.tsx
export function ProductHeader({ product }) {
const avgRating = product.reviews.length > 0
? (product.reviews.reduce((sum, r) => sum + r.rating, 0) / product.reviews.length).toFixed(1)
: null
const reviewCount = product.reviews.filter(r => r.status === 'approved').length
return (
<div className="product-header">
<h1>{product.name}</h1>
{avgRating && (
<div className="rating-summary">
<StarDisplay rating={avgRating} />
<span>{avgRating} ({reviewCount} reviews)</span>
</div>
)}
</div>
)
}