Sharkly logo
Shopify SEO

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.

Sharkly Team March 14, 2026 17 min read

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_data filter does not output AggregateRating. 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 brand property. 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.0 conversion is required to produce a valid decimal. Some custom snippets skip this and output 8999 as the price, which fails Google's validation checks.

  • Collection pages have no schema at all. Shopify provides nothing for collection pages by default. Adding CollectionPage or ItemList schema 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_data filter 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 second Product schema 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.

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 {{ product | structured_data }} and a custom snippet together

Two Product schema blocks on the same page — Google flags conflicting structured data and may ignore both

Remove the Liquid filter call from your product template before deploying the custom snippet. Search structured_data in your theme files to find all instances.

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 divided_by: 100.0. The .0 is not optional.

Protocol-relative image URLs (//cdn.shopify.com/...)

Google's schema validator rejects URLs without explicit protocol — images not validated

Append prepend: "https:" after image_url in every image reference

Description output without strip_html

Raw HTML tags in JSON-LD produce malformed JSON — entire schema block rejected by validator

Always pipe description through strip_html before json

AggregateRating block present with zero reviews

A ratingValue with no actual reviews is a spam signal — Google may suppress rich results for the page

Wrap aggregateRating in a conditional that only outputs when reviews exist and review count is greater than zero

Missing priceValidUntil

Price rich results eligibility reduced — Google may not display price in the listing

Add a dynamic priceValidUntil set to end of following year. Never use a past date.

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 variant.barcode to the gtin property. If products don't have barcodes, omit — do not fabricate GTINs.

Schema not re-checked after theme updates

Theme update reinstates structured_data filter, overwrites template include, or breaks Liquid conditionals — schema silently reverts to default or breaks entirely

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 {% unless forloop.last %},{% endunless %} to conditionally append the comma

Quick reference: which schema type for which Shopify page

Page type

Schema type

Rich result unlocked

Priority

Product page

Product + Offer

Price, availability in listing

Critical — implement first

Product page with reviews

Product + AggregateRating

Star ratings in listing

Critical — highest CTR impact

Collection page

ItemList

Product carousel / sitelinks

High

Product or collection page

BreadcrumbList

Breadcrumb path in listing

Medium

Blog post / guide

Article

Article rich result, date

Medium

Homepage

Organization + WebSite

Sitelinks search box

Low-medium

FAQ page or product FAQ section

FAQPage

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