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 aggregateRating stays 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).