Shopify

Structured data on Shopify without an app: a Liquid walkthrough

Liquid templates generating JSON-LD for ecommerce structured data.

Shopify structured data does not always need another app.

Apps can help, but they can also create duplicate schema, slow down the storefront, or output generic markup that does not match the theme. For many stores, the cleanest approach is to generate JSON-LD directly in Liquid.

That gives you control.

What structured data should cover

For a typical Shopify storefront, I usually look at:

  • Product
  • Offer
  • AggregateRating, if real review data exists
  • BreadcrumbList
  • CollectionPage
  • FAQPage, where FAQs are visible on the page
  • Organization
  • WebSite

The important rule: structured data should describe content that actually exists on the page.

Product schema in Liquid

A simplified Product schema might look like this:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "description": {{ product.description | strip_html | json }},
  "url": {{ shop.url | append: product.url | json }},
  "image": [
    {% for image in product.images limit: 5 %}
      {{ image | image_url: width: 1200 | prepend: "https:" | json }}{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ],
  "offers": {
    "@type": "Offer",
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
    "availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
    "url": {{ shop.url | append: product.url | json }}
  }
}
</script>

The exact implementation depends on the store, but this is the shape.

Breadcrumb schema is useful because it helps search engines understand hierarchy.

In Liquid, you can generate it from the current collection or fallback paths:

{
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": {{ shop.url | json }}
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": {{ collection.title | json }},
      "item": {{ shop.url | append: collection.url | json }}
    }
  ]
}

If a product can appear in multiple collections, be careful. Schema should reflect the visible breadcrumb path, not a random collection association.

FAQ schema

FAQ schema should only be added when the questions and answers are visible to users.

Good sources include:

  • theme sections
  • metafields
  • metaobjects
  • product-specific FAQ blocks

Avoid adding hidden FAQ schema just because it used to be a common SEO trick.

Common mistakes

The mistakes I see most often:

  1. Duplicate Product schema from theme and app.
  2. Missing currency or incorrect price format.
  3. Review schema without real review content.
  4. FAQ schema for hidden content.
  5. Canonical URL and schema URL disagreeing.
  6. Collection context leaking into product schema incorrectly.

Structured data should be boringly accurate.

How to validate

After implementation, test:

  • rendered page source
  • Rich Results Test
  • Schema Markup Validator
  • Search Console enhancements
  • several product variants
  • in-stock and out-of-stock states

Do not validate only one product and assume the template works everywhere.

Why I prefer native Liquid when possible

Native Liquid schema is easier to review, easier to version control, and easier to adapt to the actual store. It avoids another app dependency for something that is fundamentally template output.

Apps are useful when a merchant needs a UI. But when a developer is already working in the theme, Liquid often gives the cleaner result.