Short answer. Search Console splits product structured data into two rich-result reports. Product snippets enrich your ordinary search result; merchant listings make you eligible for the shopping surfaces, and Google requires an Offer with a price above zero because the merchant has to be the seller. Which report your Shopify products land in depends on where the offer sits in your markup — and on a product with variants, that is not the top level.
Two reports sit under the Shopping section of Search Console with almost the same subject, and neither explains why a product is in one and not the other. The difference is not cosmetic: one is about how your existing result looks, the other about whether Google can treat you as a seller at all. It is also one of the ways a Shopify store loses ground on Google without anything appearing to break.
This confuses people who are otherwise doing everything right. In a Shopify Community thread from 12 November 2024 a merchant reported that after adding their store to Search Console they had "2 non-critical issue detected about Product snippets and Merchant listings", and asked what to do about it. The single reply asked them to click through and report back what the issues said. Nobody answered, and the thread ends there.
What the two reports actually are
Google's Shopping reports and tools page defines them in one line each, and the wording is precise. The product snippets report "is a rich result report for Product snippet structured data found on your site", covering results shown "with visual enhancements—for example, product ratings and prices". The merchant listings report "is a rich result report for Merchant listing structured data found on your site", and those listings "enable more full-featured search results and always include a price" and "might appear in a carousel of similar products from different sellers, or in a knowledge panel in search results".
So a product snippet decorates the blue link you already have. A merchant listing puts you into a different class of result — the carousels and knowledge panels where shoppers compare sellers. For a store, that second one is the commercially interesting surface, and it is the one with the stricter entry condition.
The line between them is the offer
Google's merchant listing documentation states the distinction directly:
"Product snippets accept an
OfferorAggregateOfferbut merchant listings require anOfferas the merchant has to be the seller of the product."
And, in the same document, a threshold that is easy to trip over: "Unlike product snippets, merchant listing experiences require a price greater than zero."
Read together, those two sentences say something useful. An AggregateOffer — a price range, the shape a review site or a comparison page would use — is enough to be a product snippet and not enough to be a merchant listing. And a price of zero does not merely look odd; it puts the item below the floor for the report that matters. We wrote that specific failure up separately, because on Shopify a zero reaches Google more often than you would expect: see "Invalid value for category [price]", which is the Merchant Center side of the same number.
Everything else Google asks for inside that Offer follows the same logic — the properties are recommended rather than required, so a missing one produces a warning rather than an exclusion. The priceValidUntil field is the clearest example: absent on a stock Shopify theme by construction, reported under a heading that means "this could be better" rather than "this is broken", and therefore left alone for years.
Where Shopify actually puts the price
Here is the part that makes this a Shopify question rather than a general one, and it is documented by Shopify rather than by Google. The structured_data Liquid filter — the one Dawn calls in sections/main-product.liquid, as a single {{ product | structured_data }} line — describes its own output like this:
"Product objects are output as a schema.org
Productif they have no variants, and aProductGroupif they have one or more variants."
That one sentence explains a great deal of confusion. On a product with variants, the top-level entity on your page is not a Product at all. It is a ProductGroup, and the offers live one level down, inside hasVariant — one offer per variant, each with its own price and its own ?variant= URL. Nothing sits at the top level saying "this product costs this much", because with several variants there is no single answer to that question.
I checked four live Shopify storefronts on 5 September 2026, fetching each product page as Googlebot and parsing every application/ld+json block in the served HTML. Two of them emitted a ProductGroup with no top-level offers and the price inside each variant, exactly as the filter documents. The other two emitted no product structured data at all in the served HTML — one returned only a CollectionPage and an FAQPage on a product URL. Not one of the four emitted the shape most advice assumes: a plain Product with a price at the top. Four stores is a small sample and I am not presenting it as a rate; it is enough to show that the assumed shape is not the default one, which is the only claim this section needs.
The practical consequence is that any check which reads only the top-level entity will report that your product page has no price, no availability and no identifiers, on a page that is marked up correctly. That is worth knowing before you act on such a report — and it is why our own scanner reconciles the group and its variants into one view before judging a page, rather than reading the outer object alone.
Want to know what else is silently wrong across your catalogue? We check every product page in your public catalogue for the Google visibility failures that never show up in Shopify admin — read-only, no admin access, about 2 minutes. → free scan
Variants are a declaration, not a detail
Google treats variant relationships as something you state deliberately. Its product variant documentation asks for the ProductGroup class with variesBy, hasVariant and productGroupID, where productGroupID is "the identifier of the product group (also known as the parent sku)". It also sets a structural precondition that Shopify happens to satisfy: "The site must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)."
And it connects the two subjects of this page explicitly: "Adding this markup also makes your products eligible for display with variant information in merchant listing experiences." So variant markup is not a decorative extra beside the merchant listing question — it is part of it.
What that means in practice is that the shape of your product markup follows your merchandising. A product with options emits a group; the same product with its options removed emits something else. In the stores I looked at, a product whose only variant was the default one still emitted a ProductGroup, but with no variesBy property — a group that varies by nothing.
Reading the combination of reports you have
The most useful diagnostic on that Search Console page is not either report on its own but which of the three exist. Google documents a third one, Merchant opportunities, which "shows recommendations for improving how your online shop appears on Google" and appears "if you have an eligible site that sells physical goods". Then it documents what the combinations mean.
If Merchant opportunities is there and the two rich-result reports are not, Google says the cause is that either "Google hasn't found structured data on your site, or Google found structured data, but the sample data used by Search Console to generate reports doesn't contain any entity data". That second clause matters and is easy to miss: the reports are built from a sample, so an absent report is not proof that no page on your store carries the markup.
The reverse combination has its own meaning. If the product reports are there and Merchant opportunities is not, Google says it "found structured data on your site, but doesn't recognize your site as an online shop that sells physical goods" — a statement about how your site is classified as a whole, not about any single product page.
Neither of those is an error message, which is precisely why they go unread. They are the closest thing Search Console offers to telling you what it thinks your store is.
How to check your own store in 5 minutes
- Read which reports exist at all. Open the Shopping section of Search Console and note which of Merchant listings, Product snippets and Merchant opportunities are present. The combination is the diagnosis, and an absent report carries as much information as a present one.
- View source on a product with variants. Read the JSON-LD in the served HTML, not in a rendered inspector — what a browser assembles and what a crawler receives are not always the same document. Check whether the top-level entity says
ProductorProductGroup. - Find where the offer actually sits. If it is a
ProductGroup, the offers are insidehasVariant, one per variant. Confirm each carriesprice,priceCurrencyandavailability, and that no price is zero. - Count the product entities on the page. List the top-level type of every
application/ld+jsonblock. A page declaring aProductGroupfrom the theme and a separateProductfrom an app is describing the same item twice, in two voices. - Compare the report against your catalogue size. Put the number of valid items in the Merchant listings report next to the number of products you actually sell. A large gap points at a subset of products whose markup differs from the rest, which is a more tractable problem than a store-wide one.
Fixing each cause
No product markup in the served HTML. This is the case worth checking first, because it is total rather than partial. If a product URL returns no Product or ProductGroup at all, no amount of tuning elsewhere matters. It usually means the theme's markup was removed during customisation, or that the page is assembled client-side. Google's reports are built from what its crawler receives, so markup that only exists after JavaScript runs is a gamble you do not need to take.
An offer that cannot clear the merchant-listing bar. A missing price, a price of zero, or an AggregateOffer where an Offer is required all keep an item out of the merchant listing report while leaving it eligible as a product snippet. That asymmetry is why a store can look fine in one report and be absent from the other.
Two entities describing one product. When a review app, a rich-snippet app or a theme section adds its own Product block beside the theme's ProductGroup, the page makes two claims about one item. Find the second block before editing the first: removing an app's output is usually easier than making the theme agree with it. If the two blocks disagree about price or availability, the consequences reach past these reports and into Merchant Center — that is the mechanism behind the on-page price mismatch trap.
Markup that is present but does not parse. A single stray character voids an entire JSON-LD block, and Search Console reports that separately, with its own literal label. If your reports are empty and you expected them not to be, rule this out before assuming an eligibility problem: "Unparsable structured data" covers what produces it on Shopify.
Why this keeps happening
None of the changes described here look like changes to Google. Installing a reviews app, letting a theme update land, retiring the last variant of a product, or bringing a developer in to customise a product template are ordinary operations, and each can move a page across the line between the two reports. Nothing in Shopify admin reports the shape of your structured data, and Search Console will show the consequence at its own pace, weeks later, in a report that has no alert attached to it. The reader's problem here is not really making the fix once — it is that the same set of routine decisions will do the same thing again, quietly, and nothing in the stack is watching for it.
To be straightforward about our own boundary: StoreCanary does not tell you which Search Console report a page appears in. That is Google's classification and only Google reports it. What we do check, on every product page in your public catalogue, is the input that classification is made from — whether product markup is present in the served HTML at all, whether it parses, whether the offer data is complete once the group and its variants are read together, and whether the availability and price it declares match what your store is really doing. Search Console remains the instrument for the verdict; these reports are the rows worth watching.
Next in this cluster
Not sure what else is silently wrong on your store right now?
Scan my store for free