Skip to main contentSkip to Content
Skip to Content

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.

JSON-LD is not visible content. It does not affect your page's visual design. It is read only by crawlers and tools like the Rich Results Test. Adding it has zero impact on page performance.

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.

This only works when your review app stores data in Shopify's standard Metaobjects. If your review app stores data on its own server, 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.

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 confirmaggregateRating is 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 Product schemas on one page. Many themes already include Product JSON-LD. Adding a second block with different data confuses Google. Check your theme's snippets/ directory for existing structured data before adding new blocks.
  • Price without currency. priceCurrency is required alongside price in the Offer. Omitting it causes a validation error.
  • Rating outside the stated range. If bestRating is 5 but ratingValue is 4.7, that is valid. If ratingValue exceeds bestRating, 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. An aggregateRating with zero reviews is invalid per Google's spec. Always guard with if rating != blank before outputting the block.
Marius Korbmacher

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.

Learn more →