Structured Data for Shopify: The Product Schema Google Actually Reads
·
Complete guide to schema.org structured data for e-commerce: Product, Offer, Reviews, Breadcrumbs, Organization. Google-focused strategy, JSON-LD examples, Shopify apps, validation tools, and best practices.
Almost every Shopify store already emits Product structured data — the theme does it for you. The problem is rarely "missing schema." It is conflicting, incomplete, or stale schema that makes a product ineligible for rich results and Google Merchant free listings. This guide is Shopify-specific: the exact fields Google reads, the markup for variants that most tutorials skip, and the two traps that silently disqualify real stores.
Why your store probably already has structured data — and why it's wrong
Shopify's Online Store 2.0 themes (Dawn and its forks) ship a product JSON-LD block inside the product section. The moment you install an SEO app (Smart SEO, JSON-LD for SEO, SearchPie), a second Product block is injected. Google now sees two Product objects on one URL, picks one non-deterministically, and often reads the one with the weaker data — no priceValidUntil, no shipping, wrong currency. You didn't lose the markup; you poisoned it.
The first fix on any Shopify store is not "add schema" — it's "find the duplicate." View source, search application/ld+json, and count the Product blocks. If there is more than one, disable the theme's or the app's — never run both.WS24 — first thing we check on a structured-data audit
The Product + Offer fields Google actually requires
For a product to be eligible for rich results and the free Merchant listings in the Shopping tab, Google needs more than name/price. The fields below are the ones whose absence shows up as "Non-critical issues" in the Rich Results Test and as warnings in Search Console's Merchant listings report:
offers.priceandoffers.priceCurrency— must match the price the shopper actually sees (see the Markets trap below).offers.availability— a schema.org URL (InStock/OutOfStock), not the string "in stock".offers.priceValidUntil— omit it and Google may drop the price from the snippet.offers.shippingDetailsandoffers.hasMerchantReturnPolicy— required for Merchant listings eligibility; missing = warnings, not errors, but they cost you the enhanced listing.aggregateRating/review— only if the reviews are genuinely collected from customers. Self-serving markup (a store rating itself) is ignored under Google's review-snippet policy.
Correct Product JSON-LD, rendered from the Shopify object
Do not hardcode values — render them from the live product object so price, stock, and SKU never drift from the catalog. This is a real snippet you can drop into a snippets/product-schema.liquid and render once from the product template:
{%- assign v = product.selected_or_first_available_variant -%}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": {{ product.title | json }},
"image": {{ product.featured_image | image_url: width: 1200 | prepend: "https:" | json }},
"description": {{ product.description | strip_html | truncate: 300 | json }},
"sku": {{ v.sku | json }},
"brand": { "@type": "Brand", "name": {{ product.vendor | json }} },
"offers": {
"@type": "Offer",
"url": {{ shop.url | append: product.url | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"price": "{{ v.price | divided_by: 100.0 }}",
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
"priceValidUntil": "{{ 'now' | date: '%Y' | plus: 1 }}-12-31"
}
}
</script>
Two details that matter: v.price is in cents, so divide by 100; and cart.currency.iso_code — not shop.currency — is what makes the currency follow the buyer's market. That second point is the trap.
The Shopify Markets price/currency mismatch
If you sell in multiple currencies with Shopify Markets, the storefront shows a localized price while a naive schema block still prints shop.currency and the base price. Google fetches the page as a buyer in that market, sees USD 79.99 in the markup but €74,00 on the page, flags a price mismatch, and suppresses the rich result. Always source both price and priceCurrency from the same market-aware objects (cart.currency + the presentment price), or serve market-specific markup. This is invisible in a single-currency test and is the single most common reason a Markets store loses its price snippet.
Variants: the ProductGroup most guides never mention
A t-shirt in five sizes and three colors is not one product to Google — it is a ProductGroup of variants. Marking it as a single Product collapses the variants and can trigger "price range" ambiguities. Use ProductGroup with hasVariant, tying each variant back with variesBy:
{
"@context": "https://schema.org/",
"@type": "ProductGroup",
"name": "Merino Base Layer",
"productGroupID": "BASE-MERINO",
"variesBy": ["https://schema.org/size", "https://schema.org/color"],
"hasVariant": [
{
"@type": "Product",
"sku": "BASE-MERINO-M-BLK",
"size": "M", "color": "Black",
"offers": {
"@type": "Offer", "price": "89.00", "priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
/* one entry per variant — render with a {% for v in product.variants %} loop */
]
}
Where to inject it — and where not to
- Product schema → the product template only (
sections/main-product.liquidor a snippet it renders). Never intheme.liquid, or you emit an empty/invalid Product on every non-product page. - BreadcrumbList → global, in
theme.liquid, built fromcollection/productcontext so the path is real. - Organization → once, on the homepage, with your real
logo,sameAssocial profiles, and contact — not on every page.
Validate, then monitor — they are different jobs
The Rich Results Test tells you a single URL is eligible. It does not tell you Google is using it. For that, watch Search Console's Merchant listings and Product snippets enhancement reports over the following weeks — valid count climbing, warnings falling. If you run Google Merchant Center, the same product feed surfaces the same errors with clearer wording; fix them there and the organic markup usually clears too.
Structured data is not a ranking hack — it is how you make Google render your price, stock, and rating instead of guessing. On Shopify the win is rarely in adding markup; it is in removing the duplicate, matching the market currency, and modeling variants the way Google's catalog actually expects.