Structured data deep dive - Product, Offer, and Review schema
Phase 2 introduced structured data; this is the working-depth version - what a complete Product markup actually contains, how the pieces nest, and where implementations go wrong in practice.
How the pieces nest
Product schema is a small hierarchy. The Product object carries identity: name, description, image, brand, sku, and ideally gtin (the global identifier that lets Google connect your listing to the same product everywhere - it matters more every year). Inside it, an Offer carries the commercial facts: price, priceCurrency, availability, and increasingly shippingDetails and hasMerchantReturnPolicy - Google now uses these for the shipping/returns annotations in listings, and stores providing them get richer results. Alongside, AggregateRating and Review carry social proof: the stars in the search listing come from aggregateRating (ratingValue + reviewCount), almost always output by your review app.
The practical checklist for a "complete" product page: Product with name/image/brand/gtin, Offer with price/currency/availability, aggregateRating if reviews exist, shipping and returns details if you can. Each present field is a feature in the search listing; each absent one is a competitor's advantage.
Variants: the honest hard part
One page, twelve variants, which price goes in the markup? The common answer - markup for the default variant - works but undersells range. Better options exist: AggregateOffer (declaring lowPrice/highPrice across variants) or, the direction Google is pushing, ProductGroup markup with per-variant Product entries sharing a productGroupID. Most Shopify themes don't do this yet; if variants with different prices are your reality (they are, in fashion), this is a legitimately valuable theme improvement to brief - and a place to check what your theme currently outputs rather than assume.
Where implementations actually break
Three recurring failure patterns from real audits. Two sources, one page: theme outputs Product schema, a review app outputs its own, Google sees conflicting declarations. One source must win - usually configure the app to inject only rating data into the theme's markup, or disable one entirely. The frozen field: markup hardcoded during a customization years ago - price updates on the visible page but not in the JSON-LD. Mismatches between markup and page are policy violations, not just bugs. Markup for content that isn't there: review markup with no visible reviews, or a "sale" price the page doesn't show. Google's rule is simple and enforced: markup describes the visible page, exactly.
The verification loop is the one from Phase 3 - Rich Results Test per template after changes, GSC merchant-listings report monthly for scale. Template-driven means errors arrive in batches and heal in batches.