SEO tutorial
How to add JSON-LD structured data in Shopify Liquid: products, reviews, and breadcrumbs
Structured data tells Google exactly what your page contains - product name, price, availability, star ratings, breadcrumb trail. When implemented correctly in Liquid, it is part of your HTML on the first byte, Googlebot reads it on the first crawl, and you become eligible for rich snippets in search results. This guide covers the three most impactful schemas for Shopify stores, with copy-paste Liquid code for each.
Audience: Shopify developers, theme developers, SEO practitioners. Reading time: ~10 minutes.
1. What JSON-LD is and why it belongs in <head>
Structured data is machine-readable markup that describes the content of a page to search engines. Google supports three formats (JSON-LD, Microdata, RDFa), but recommends JSON-LD as the preferred format for all new implementations.
JSON-LD is a <script type="application/ld+json"> block containing a JSON object that follows schema.org vocabulary. Unlike Microdata, it does not require you to annotate individual HTML elements - it lives separately from your visible markup.
Place it inside <head> - or at minimum, before the closing </body> tag. Google does not require <head> placement, but it is best practice so the data is available as early as possible in the parse.
In Shopify, the right place is a snippet included from your theme's layout/theme.liquid file, rendered conditionally based on the current template.
2. Product schema
The Product rich result requires at minimum: name, an Offer with price and priceCurrency, and either review or aggregateRating (for star ratings). Here is a complete base implementation:
{%- comment -%}
snippets/structured-data-product.liquid
Include from layout/theme.liquid inside <head>:
{%- if template == 'product' -%}
{%- render 'structured-data-product' -%}
{%- endif -%}
{%- endcomment -%}
<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: 500 | json }},
"sku": {{ product.selected_or_first_available_variant.sku | json }},
"brand": {
"@type": "Brand",
"name": {{ shop.name | json }}
},
"offers": {
"@type": "Offer",
"url": {{ canonical_url | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 }},
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
"itemCondition": "https://schema.org/NewCondition"
}
}
</script>Note the divided_by: 100.0 on the price - Shopify stores prices in cents as integers, so 1999 represents $19.99. Dividing by 100.0 (not 100) forces a decimal result.
3. Adding aggregateRating from Metafields
Extend the Product schema above with an aggregateRating property. When review data is stored in Shopify as standard Metaobjects, the aggregate rating is available in Liquid via product.metafields.reviews.rating:
{%- assign rating = product.metafields.reviews.rating.value -%}
{%- assign rating_count = product.metafields.reviews.rating_count.value -%}
<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: 500 | json }},
"offers": {
"@type": "Offer",
"url": {{ canonical_url | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 }},
"availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}"
}
{%- if rating != blank -%}
,"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{ rating }}",
"bestRating": "5",
"worstRating": "1",
"reviewCount": "{{ rating_count }}"
}
{%- endif -%}
}
</script>The if rating != blank guard ensures the aggregateRating property is only output when review data actually exists - a product with no reviews outputs valid Product schema without any rating properties, which is correct per Google's spec.
product.metafields.reviews.rating will be blank in Liquid. The aggregate rating only exists on an external API, meaning structured data must be generated by JavaScript - with the crawlability problems that entails.FiveOh Reviews on Metaobjects stores ratings in Shopify's standard Metaobjects - so your aggregateRating JSON-LD is rendered in Liquid, readable by Googlebot on first crawl.
Get more information →4. Individual Review markup
Google can display individual review snippets in search results when schema.org/Review objects are nested inside the Product schema. These are separate from the aggregate rating and give Google richer context about your review content.
{%- assign reviews = product.metafields.reviews.product_reviews.value -%}
{%- if reviews != blank -%}
,"review": [
{%- for review in reviews limit: 5 -%}
{
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "{{ review.fields.rating.value }}",
"bestRating": "5",
"worstRating": "1"
},
"name": {{ review.fields.body.value | truncate: 100 | json }},
"reviewBody": {{ review.fields.body.value | json }},
"author": {
"@type": "Person",
"name": {{ review.fields.author.value | json }}
},
"datePublished": "{{ review.fields.date.value | date: '%Y-%m-%d' }}"
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
{%- endif -%}Include this inside the Product JSON-LD object, after the aggregateRating block. The limit: 5 keeps the JSON-LD block to a reasonable size - Google does not require all reviews to be in the structured data, and a smaller block reduces page weight.
5. BreadcrumbList schema
BreadcrumbList structured data enables the breadcrumb trail to appear in your Google Search listing - replacing or supplementing the URL display. It is especially valuable for product pages reached via a collection.
{%- comment -%}
snippets/structured-data-breadcrumb.liquid
Render this when a collection context is available (e.g. on product pages
linked from a collection, or on collection pages).
{%- endcomment -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": {{ shop.name | json }},
"item": {{ shop.url | json }}
}
{%- if collection -%}
,{
"@type": "ListItem",
"position": 2,
"name": {{ collection.title | json }},
"item": {{ shop.url | append: collection.url | json }}
}
,{
"@type": "ListItem",
"position": 3,
"name": {{ product.title | json }},
"item": {{ shop.url | append: product.url | json }}
}
{%- else -%}
,{
"@type": "ListItem",
"position": 2,
"name": {{ product.title | json }},
"item": {{ shop.url | append: product.url | json }}
}
{%- endif -%}
]
}
</script>The collection object is available in Liquid when a product is browsed in the context of a collection (via a /collections/my-collection/products/my-product URL). It will be blank when the product is accessed directly via /products/my-product.
This breadcrumb markup, like all structured data in FiveOh Reviews on Metaobjects, is output server-side - Googlebot picks it up immediately without needing to execute JavaScript.
Get more information →6. Validating with Google's Rich Results Test
After implementing structured data, validate it before expecting results in Search:
- Rich Results Test: Enter your product URL. Shows detected schemas, warnings, and errors. Use this to confirm
aggregateRatingis present and valid. - Schema.org Validator: Paste your raw JSON-LD to check against the schema.org spec directly - useful for catching type mismatches before deploying.
- Google Search Console - Rich results status: After Google re-crawls your pages, the "Rich results" report in Search Console shows which URLs have valid structured data and which have errors.
After deploying, use Search Console's URL Inspection to request re-indexing for key product pages. Stars typically start appearing in search results within 2–6 weeks of Google processing the updated structured data.
7. Common mistakes
- Duplicate
Productschemas on one page. Many themes already include Product JSON-LD. Adding a second block with different data confuses Google. Check your theme'ssnippets/directory for existing structured data before adding new blocks. - Price without currency.
priceCurrencyis required alongsidepricein the Offer. Omitting it causes a validation error. - Rating outside the stated range. If
bestRatingis 5 butratingValueis 4.7, that is valid. IfratingValueexceedsbestRating, Google rejects the schema. - Generating structured data in JavaScript. If the JSON-LD block is injected by a client-side script, Googlebot reads it in its second-pass rendering queue - potentially delayed by days. Always output structured data in Liquid, not JavaScript.
- Using
reviewCount: 0. AnaggregateRatingwith zero reviews is invalid per Google's spec. Always guard withif rating != blankbefore outputting the block.
Written by Marius Korbmacher
Lead Developer at FiveOh Reviews on Metaobjects
FiveOh Reviews on Metaobjects
Reviews stored in Shopify. Rendered in Liquid. Yours to keep.
The review app that writes to Shopify's standard product review Metaobjects - server-side rendering, no JavaScript widget, no external dependency, no vendor lock-in.
