Shopify Schema Markup: How to Add Product and Collection Structured Data the Right Way
Schema markup is the structured data layer underneath your Shopify store that tells Google exactly what your products are — price, availability, reviews, brand — in a format it can read without guessing. Here's how to implement it correctly, what Shopify handles for you, and where it silently fails.
What is schema markup, and why does it matter for your Shopify store?
Schema markup is structured data — a block of JSON code added to your page that describes your content in a vocabulary Google understands natively. For a product page, that means telling Google not just that you sell something called "Running Shoe Model X", but that it is a Product, it costs £89.99, it is currently InStock, it carries a 4.7-star aggregate rating from 312 reviews, and the brand is a specific organisation you can verify.
Without schema, Google has to infer all of this from your page content — reading your HTML, guessing at your price format, estimating your availability from copy like "ships in 2–3 days". With schema, you hand it the answer sheet directly.
The practical consequences are significant. Correct product schema enables rich results — the enhanced search listings that show star ratings, price, availability, and shipping information directly in Google's results page, before anyone clicks. Research consistently shows rich results generate meaningfully higher click-through rates than standard blue-link listings for the same ranking position. And as with meta descriptions, CTR feeds directly into Google's Navboost user-behaviour scoring system — which means schema improvements compound over time through the same mechanism that rewards any click-quality improvement.
For Shopify stores specifically, there is another layer of complexity: Shopify's built-in schema handling is inconsistent across themes, partially broken in some common configurations, and almost always missing the signals that separate a baseline implementation from one that earns rich results. This guide covers exactly what to implement, how, and why.
How Google uses structured data — and what it actually unlocks
Before writing a line of Liquid, it is worth being precise about what schema does and does not do for your rankings.
Schema markup is not a direct ranking factor in the sense that adding it will not, by itself, move your product pages up in the rankings. Google has been explicit about this. What schema does is:
Enable rich results eligibility. You cannot get price, availability, or review stars shown in your search listing without valid structured data. Google only shows these enhancements for pages that pass schema validation.
Improve click-through rate. A listing showing "£89.99 · ★★★★½ · In Stock" next to your competitors' plain blue links will generate more clicks for the same ranking position. Higher CTR → better Navboost signals → rankings that hold and improve over time.
Reduce Google's reliance on inference. When Google doesn't have to guess at your product data, it indexes your pages more confidently and surfaces them more consistently for relevant queries. Ambiguity in your content creates ambiguity in how you're ranked.
Enable Merchant Centre integration. If you connect your Shopify store to Google Merchant Centre (essential for Shopping ads), schema on your product pages acts as a second data layer that Merchant Centre cross-references. Discrepancies between your schema and your Merchant Centre feed cause disapprovals.
The bottom line: schema is infrastructure. It is not a quick-win tactic. Done correctly, it is the technical foundation that allows everything else — your content, your backlinks, your keyword targeting — to perform at full potential in a search results page that has increasingly rewarded structured product data.
What Shopify does automatically — and where it falls short
Modern Shopify themes (Dawn and later) include a built-in structured data output via the {{ product | structured_data }} Liquid filter. This handles the basics: product name, description, price, and availability. For the majority of stores running a default theme without modification, some schema is already being output.
The problems begin immediately below that surface level:
No aggregate reviews. Shopify's native
structured_datafilter does not outputAggregateRating. If you use a reviews app (Judge.me, Loox, Okendo, Yotpo, Stamped), your stars are on your page but not in your schema unless you explicitly add them or your app injects its own block. You are not getting review rich results without this.No brand data. The default output omits the
brandproperty. Google uses this to understand product provenance, connect your products to brand entities in the Knowledge Graph, and serve brand-filtered searches correctly.No GTIN / MPN / barcode. Product identifiers (Global Trade Item Numbers, Manufacturer Part Numbers) are powerful disambiguating signals. They allow Google to confirm your product is the same as the one described in external sources — product databases, manufacturer sites, review aggregators. Shopify stores these in variant metafields and barcode fields, but they are not output by the default filter.
Price schema uses the wrong format in some themes. Shopify stores prices in pence/cents as integers (e.g. 8999 for £89.99). The
divided_by: 100.0conversion is required to produce a valid decimal. Some custom snippets skip this and output8999as the price, which fails Google's validation checks.Collection pages have no schema at all. Shopify provides nothing for collection pages by default. Adding
CollectionPageorItemListschema to your collections is entirely manual — but it is one of the highest-leverage schema additions for stores with strong collection SEO strategies.The
structured_datafilter cannot be customised. If you are using Dawn or a theme that relies on the Liquid filter, you have no way to add additional properties to the output. You either replace it entirely with a custom snippet, or you output a secondProductschema block (which creates duplication warnings in Google's Rich Results Test).
The recommendation for any store taking SEO seriously: disable or replace Shopify's native structured data output with a custom snippet you fully control. This is a one-time setup that pays dividends for the lifetime of the store.
Product schema: the correct implementation
Save the following as snippets/schema-product.liquid and include it in your product template with {% render 'schema-product' %}.
If your theme uses {{ product | structured_data }}, find it in your product template (usually sections/main-product.liquid or templates/product.json) and remove it before adding this snippet — or you will output two Product schema blocks for the same page.
{% comment %}
Product schema — full implementation with reviews, brand, GTINs, and shipping.
Save as: snippets/schema-product.liquid
Include in product template: {% render 'schema-product' %}
Remove any {{ product | structured_data }} calls in your theme before using this.
{% endcomment %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 5000 | json }},
"url": {{ shop.url | append: product.url | json }},
"image": [
{{ product.featured_image | image_url: width: 1200 | prepend: "https:" | json }}
{% for image in product.images limit: 5 %}
{% unless image == product.featured_image %}
, {{ image | image_url: width: 1200 | prepend: "https:" | json }}
{% endunless %}
{% endfor %}
],
"sku": {{ product.selected_or_first_available_variant.sku | json }},
{% if product.vendor != blank %}
"brand": {
"@type": "Brand",
"name": {{ product.vendor | json }}
},
{% endif %}
{% assign variant = product.selected_or_first_available_variant %}
{% if variant.barcode != blank %}
"gtin": {{ variant.barcode | json }},
{% endif %}
"offers": {
"@type": "Offer",
"price": {{ variant.price | divided_by: 100.0 | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
"itemCondition": "https://schema.org/NewCondition",
"url": {{ shop.url | append: product.url | json }},
"priceValidUntil": "{{ "now" | date: "%Y" | plus: 1 }}-12-31",
"seller": {
"@type": "Organization",
"name": {{ shop.name | json }}
}
}
{% if product.metafields.reviews.rating != blank %}
,
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": {{ product.metafields.reviews.rating.value.rating | json }},
"reviewCount": {{ product.metafields.reviews.rating_count | json }},
"bestRating": "5",
"worstRating": "1"
}
{% endif %}
}
</script>
Breaking down each property — and why it's there
name and description
product.title and product.description are the baseline. The strip_html filter on the description is non-optional — Shopify product descriptions frequently contain HTML formatting tags, and outputting raw HTML inside JSON-LD produces invalid schema that Google's validator will reject. The truncate: 5000 limit prevents edge cases where very long descriptions cause JSON parse errors.
image as an array
Google's product rich results guidelines specify that the image should be crawlable, large enough to trigger rich results (minimum 160px, recommended 1200px), and ideally provided as an array when multiple images are available. The loop above outputs up to five images from your product gallery. The prepend: "https:" is required because Shopify's image_url filter returns protocol-relative URLs (//cdn.shopify.com/...) by default — which are technically valid in browsers but rejected by Google's schema validator.
brand
Shopify stores the product vendor in product.vendor. Including it as a Brand entity is one of the most underused schema properties for Shopify stores. It connects your products to brand entities in Google's Knowledge Graph, which is increasingly important for brand-specific search queries ("Nike running shoes", "Le Creuset casserole"). The conditional wrapper ensures the block is omitted if vendor is blank rather than outputting an empty string, which would fail validation.
gtin from barcode
Shopify stores EAN/UPC barcodes in variant.barcode. If your products have barcodes — which most retail and wholesale products do — this is the single highest-impact addition you can make to your schema beyond the baseline. GTINs allow Google to cross-reference your product against its product database, confirm identifiers, and surface your listing in Shopping-integrated search results with far higher confidence. The conditional wrapper omits the property if the barcode field is empty, keeping the schema valid for products without barcodes.
offers — price formatting
Shopify's price is stored as an integer in pence or cents (8999 for £89.99). The divided_by: 100.0 conversion to a decimal is mandatory. Outputting "price": 8999 will cause your rich results to either fail validation or show an incorrect price. The .0 in 100.0 forces Liquid to perform floating-point division rather than integer division — without it, 8999 / 100 outputs 89, not 89.99.
priceValidUntil
Google requires priceValidUntil for price-bearing rich results to be eligible. Without it, your listing may be excluded from price-featured rich results even if all other schema is valid. The Liquid expression above dynamically sets it to the end of the following year — a safe, evergreen value that does not require manual maintenance.
aggregateRating
Review stars in search results are consistently among the highest CTR drivers for product pages. The implementation above uses Shopify's native Product Reviews metafield namespace (metafields.reviews.rating), which is populated by Shopify's own Product Reviews app and is also adopted by several third-party apps. If you use Judge.me, Okendo, Loox, or another reviews platform, check their documentation for their metafield namespace — the structure may differ, but the pattern is identical. The conditional wrapper ensures the block is omitted entirely if no reviews exist, preventing a ratingValue: null validation failure on new products.
Collection page schema: ItemList
Collection pages are one of the most overlooked schema opportunities in Shopify SEO. A collection page is a curated list of products — which maps directly to Schema.org's ItemList type. Adding this signals to Google the page's purpose, its constituent products, and the relationship between them.
Save the following as snippets/schema-collection.liquid and include it in your collection template.
{% comment %}
Collection schema — ItemList implementation.
Save as: snippets/schema-collection.liquid
Include in collection template: {% render 'schema-collection' %}
Outputs the first 20 products in the collection as ListItems.
{% endcomment %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": {{ collection.title | json }},
"description": {{ collection.description | strip_html | json }},
"url": {{ shop.url | append: collection.url | json }},
"numberOfItems": {{ collection.products_count }},
"itemListElement": [
{% for product in collection.products limit: 20 %}
{
"@type": "ListItem",
"position": {{ forloop.index }},
"url": {{ shop.url | append: product.url | json }},
"name": {{ product.title | json }},
"image": {{ product.featured_image | image_url: width: 800 | prepend: "https:" | json }},
"offers": {
"@type": "Offer",
"price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}"
}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
</script>
Notes on the collection implementation
The limit: 20 on the product loop is deliberate. Outputting every product in a large collection produces a very large JSON-LD block — which slows page load, and provides diminishing signal value. Google does not need 200 products listed in your schema to understand what the collection is about. Twenty well-structured entries are sufficient.
The {% unless forloop.last %},{% endunless %} pattern handles JSON comma placement correctly — Liquid's loop doesn't natively suppress the trailing comma that would produce invalid JSON, so this conditional drops the comma after the final item.
The collection description field deserves attention here. Most Shopify stores leave collection descriptions blank — which means the description property in your schema outputs an empty string. Google uses collection descriptions as a primary input for understanding what the collection is about and which search queries it should serve. Writing a focused, keyword-aware collection description (100–300 words) and keeping it in your Shopify admin is one of the highest-ROI content actions for collection page SEO — and it directly improves your schema output.
BreadcrumbList schema — the underused third piece
If your store has a meaningful hierarchy — products nested within collections nested within categories — BreadcrumbList schema is the third piece of the puzzle. It tells Google the navigational path to any given page, which it uses to generate breadcrumb-style rich results (the "Home > Running Shoes > Trail" path that sometimes appears under your title in search results).
{% comment %}
Breadcrumb schema — for product pages with a clear collection parent.
Save as: snippets/schema-breadcrumb.liquid
Include after your product or collection schema snippet.
Assumes the product belongs to at least one collection.
{% endcomment %}
{% if product.collections.size > 0 %}
{% assign parent_collection = product.collections.first %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": {{ shop.url | json }}
},
{
"@type": "ListItem",
"position": 2,
"name": {{ parent_collection.title | json }},
"item": {{ shop.url | append: parent_collection.url | json }}
},
{
"@type": "ListItem",
"position": 3,
"name": {{ product.title | json }},
"item": {{ shop.url | append: product.url | json }}
}
]
}
</script>
{% endif %}
How to validate your schema
Writing the snippet is only half the work. Schema that appears valid in your code can still fail Google's validation for reasons that are not always obvious — malformed JSON from a product description containing special characters, a blank required field, or a Liquid filter producing an unexpected output type.
Use these tools in sequence after deploying any schema changes:
Google Rich Results Test (
search.google.com/test/rich-results) — paste your product page URL and Google will fetch the live page, parse all structured data blocks, and tell you exactly which rich result types you are eligible for and which errors or warnings are present. This is the authoritative source.Schema.org Validator (
validator.schema.org) — useful for checking the structural validity of your JSON-LD independently from Google's rich results requirements. Catches JSON syntax errors that the Rich Results Test sometimes surfaces less clearly.Google Search Console → Enhancements — once your schema is deployed and Googlebot has crawled your pages, the Enhancements section in Search Console shows rich result coverage, errors, and warnings across your entire site. Monitor this after deployment and again after any theme updates, which frequently overwrite custom snippets.
One common failure mode worth calling out explicitly: special characters in product descriptions. If a product description contains quotation marks, ampersands, or non-UTF-8 characters, Liquid's json filter will escape them correctly — but only if applied consistently. Any concatenated string in your schema that does not pass through the json filter is a potential JSON syntax break. The rule is simple: every dynamic variable in your JSON-LD must go through | json.
Theme updates will break your custom snippets — here's what to do
This is the single most common reason well-implemented product schema stops working: a theme update overwrites the product template, reinstates {{ product | structured_data }}, and you are back to the basic Shopify default without realising it.
The correct approach is to make your schema snippets part of your theme version control process, not a one-off addition. Keep your snippet files in a repository, document in your theme notes that schema-product.liquid and schema-collection.liquid exist and must be re-included after any theme update, and add a periodic check of your Rich Results Test results as a standing SEO audit task. Search Console Enhancements will also alert you if coverage drops — but only after Googlebot has recrawled the affected pages, which introduces a lag.
If you are on a Shopify Plus plan or a custom theme, consider moving schema management to a dedicated app (Schema Plus, JSON-LD for SEO) that persists across theme updates independently. The tradeoff is less granular control — most apps output a good but not perfect implementation — weighed against zero risk of losing your schema on update.
Common schema mistakes on Shopify stores — and how to fix them
Mistake | The problem | The fix |
|---|---|---|
Using | Two | Remove the Liquid filter call from your product template before deploying the custom snippet. Search |
Price output as an integer (8999 instead of 89.99) | Fails Google's rich results price validation — listing excluded from price-featured results | Always use |
Protocol-relative image URLs ( | Google's schema validator rejects URLs without explicit protocol — images not validated | Append |
Description output without | Raw HTML tags in JSON-LD produce malformed JSON — entire schema block rejected by validator | Always pipe description through |
AggregateRating block present with zero reviews | A | Wrap |
Missing | Price rich results eligibility reduced — Google may not display price in the listing | Add a dynamic |
Collection description left blank | Schema outputs empty description string — missed signal for collection page topic and keyword relevance | Write 100–300 words of collection description in Shopify admin for every collection you want to rank |
No GTIN despite products having barcodes | Missed opportunity to cross-reference against Google's product database — reduces confidence of product identification | Map |
Schema not re-checked after theme updates | Theme update reinstates | Monitor Search Console Enhancements after every theme update. Add schema check to your post-update QA checklist. |
Trailing comma in ItemList JSON | The last item in the loop outputs a trailing comma — invalid JSON, entire block rejected | Use |
Quick reference: which schema type for which Shopify page
Page type | Schema type | Rich result unlocked | Priority |
|---|---|---|---|
Product page |
| Price, availability in listing | Critical — implement first |
Product page with reviews |
| Star ratings in listing | Critical — highest CTR impact |
Collection page |
| Product carousel / sitelinks | High |
Product or collection page |
| Breadcrumb path in listing | Medium |
Blog post / guide |
| Article rich result, date | Medium |
Homepage |
| Sitelinks search box | Low-medium |
FAQ page or product FAQ section |
| Expandable FAQ in listing | Situational |
Based on "The Science of Google Search: A Complete SEO Dissertation" and "The Complete SEO System" — grounded in verified Google patents and Schema.org specifications. Shopify Liquid implementation tested against Google's Rich Results Test and Search Console Enhancements reporting.
Ready to put this into practice?
Sharkly handles your keyword research, content strategy, and article generation — automatically.
Try Sharkly Now