Aggregation Disclosure (Headless)
Last updated: May 8, 2026
If your storefront renders BetterReviews data from Shopify metafields directly — Hydrogen, a custom React PDP, or any non-Liquid storefront — and you display a Review Group aggregate on a product page, this guide is for you.
Why this matters
The FTC's Endorsement and Reviews Guides (16 C.F.R. Part 255) require clear disclosure when a customer-visible review count or rating reflects more than just the product the customer is looking at. Showing "47 reviews" on a product page when 39 of those reviews were submitted for sibling variants — without disclosing that — risks being treated as deceptive. Your storefront, not BetterReviews, is the entity making the representation; the obligation transfers to whoever renders the surface.
What BetterReviews's first-party widget does
- Headline aggregate count (e.g. "47 reviews") — visible.
- Disclosure subtitle on the same line: "Based on 47 reviews across 8 products", or "Based on 47 reviews across the Summer Tee collection" when the merchant has set a
display_name. - Per-row attribution on each review whose
display_product_id≠ the current PDP's product id: "Reviewed: Red Shirt". - JSON-LD
aggregateRatingstays product-only.
What you should render in headless
The same three elements. The shop-level group_summary_<store_id>_<group_id> metafield contains everything you need:
// Pseudocode for a Hydrogen PDP component
const pointer = product.metafields.betterreviews.display_group_id;
if (pointer && pointer.group_id) {
const groupKey = `group_summary_${pointer.store_id}_${pointer.group_id}`;
const groupData = shop.metafields.betterreviews[groupKey];
if (groupData?.summary?.total_count) {
const subtitle = groupData.display_name
? `Based on ${groupData.summary.total_count} reviews across the ${groupData.display_name} collection`
: `Based on ${groupData.summary.total_count} reviews across ${groupData.member_count} products`;
return (
<div>
<Stars rating={groupData.summary.average_rating} />
<span>{escapeHtml(subtitle)}</span> {/* always escape display_name */}
{groupData.top_reviews.reviews.map(r => (
<ReviewRow review={r}>
{r.display_product_id !== product.id.toString() && (
<small>Reviewed: {productLookup(r.display_product_id).title}</small>
)}
</ReviewRow>
))}
</div>
);
}
}
// Fall back to per-product summary
return <ProductReviewsBlock summary={product.metafields.betterreviews.summary} />; Schema.org aggregateRating
If you emit JSON-LD on the PDP, the aggregateRating object must reflect the product's own reviews only, even when the visible widget shows group-aggregated counts. Google's product reviews policy prohibits cross-product aggregation in product schema. Use product.metafields.betterreviews.summary (per-product) for schema, never the group payload.
Sanitisation
display_name is server-side validated by BetterReviews to reject HTML / template-injection markers and capped at 100 characters. Always still escape on render — your view templates should treat it as untrusted user input.
If you opt out of group display
The merchant manages whether a product is in a group via their BetterReviews admin. If you'd rather always render per-product on your storefront, ignore display_group_id and use only the per-product summary. This is permitted; just don't mix-and-match (showing group-aggregated count without group disclosure is the noncompliant case).