Back to Blog
Guides13 min read27 July 2026

Shopify Schema Markup: What Themes Miss and How to Fix It

Shopify themes ship basic Product schema but leave out brand, GTIN, and ratings. What's missing, how to fix it, and six install mistakes.

By AI Schema Gen Team

Shopify has a structured data problem that most merchants never notice: the theme ships just enough schema to look handled, and leaves out most of what actually earns a rich result. Your product pages have Product markup. It's real, it validates, and it's missing the brand, the identifiers, the ratings, and the shipping and return details that separate a basic snippet from a competitive merchant listing.

Worse, the moment you try to fix it, you hit Shopify's specific trap: add your own Product block and you now have two, which produces duplicate-entity warnings instead of better results.

This is the Shopify-specific guide: what the default themes emit, exactly what they leave out, how to add the missing pieces without creating duplicates, and the handful of Liquid-specific mistakes that break most installs.

What Shopify themes emit by default

Modern Shopify themes, including Dawn and the other official themes, do include structured data out of the box. Themes generate product schema using Shopify's structured_data Liquid filter, which outputs JSON-LD into the page.

What you typically get by default:

  • Product schema on product pages, covering the basics: name, price, currency, availability, and URL
  • Organization schema, usually in the footer or theme.liquid, with store name, URL, and logo
  • BreadcrumbList schema on collection and product pages

That's a real foundation, and it's what enables the price and basic listing you already see in search. The problem isn't that Shopify emits nothing, it's that the default output stops at the basics.

First, verify what you actually have, because themes vary. On a product page, view source (not the element inspector, actual view-source) and search for application/ld+json. You'll see the JSON-LD blocks your theme emits. Do this on a product page, a collection page, a blog post, and the homepage, because each template behaves differently and third-party themes from the Theme Store emit more or less than the official ones. Older or heavily customized themes sometimes strip schema entirely.

What's missing

Here's the gap. Shopify's default Product output commonly omits:

  • brand, a basic identifier that helps match your product
  • sku, gtin, mpn, the identifiers that connect your listing to the exact product across shopping surfaces
  • aggregateRating and review, your review scores, invisible to search and AI even when you display them on the page
  • description and complete image sets, sometimes thin or absent
  • hasMerchantReturnPolicy and shippingDetails, the shipping and return fields that qualify you for the fuller merchant listing rather than a basic snippet

Some themes also emit Product as microdata rather than JSON-LD, which is Google's less-preferred format and harder to extend.

The pattern: defaults get you a basic product snippet. The merchant listing, the richer, more competitive shopping result, needs the fields the defaults leave out. We covered the snippet-versus-merchant-listing distinction in depth in the Product schema guide; on Shopify, the default output lands you squarely on the wrong side of it.

The duplicate-schema trap

This is the Shopify-specific mistake, and it's the single most common cause of failed installs.

Because the theme already emits a Product block, adding your own second Product block gives the page two top-level Product entities with overlapping fields. The result isn't richer markup, it's a duplicate-entity warning in the Rich Results Test and an "ambiguous data" warning in Search Console. Two competing Product schemas is worse than one incomplete one.

There are two clean ways to handle this:

Extend, don't duplicate. Rather than authoring a second complete Product block, add a complementary block that supplies only the missing entities and references the existing Product by @id, using @graph. This adds brand, ratings, and identifiers without re-declaring the fields Shopify already emits, avoiding the duplicate.

Replace deliberately. Alternatively, modify the theme's own Product output so there's still only one block, but a complete one. This means editing the product template (main-product.liquid or the equivalent section), and it needs maintaining across theme updates.

The wrong move is layering a full second Product block on top and hoping more markup means better results. On Shopify specifically, it means duplicate warnings.

Before authoring anything, audit for existing blocks. View source on a representative product page and count the Product blocks. If your theme emits one and a review app emits another and an SEO app emits a third, you may already have a duplicate problem before you add anything.

Where the data lives: metafields

The reason Shopify omits gtin, mpn, and brand by default is that Shopify's core product model doesn't have dedicated fields for all of them. The standard place to store this data is metafields.

Set up metafields for GTIN, MPN, brand, and any other attributes you want in your markup, populate them per product, and reference them in your JSON-LD. This is what turns "we sell this" into "this is exactly which product this is," which is what shopping surfaces need to match you confidently.

For variant-level data, different GTINs or availability per size or colour, you'll reference variant metafields and Shopify's variant objects. This is also where proper variant modelling with ProductGroup and hasVariant comes in, which matters for catalogues where each variant has its own identifier and stock.

Collection pages: the underused opportunity

A Shopify collection page is functionally a list of products, and it's the most commonly un-marked-up template on the platform.

Collection pages can carry ItemList markup describing the products they contain, and combined with breadcrumb markup they help search engines understand your catalogue's structure. Most stores leave collections entirely bare of structured data beyond whatever breadcrumb the theme provides. It won't produce a product rich result, that belongs on individual product pages, but it contributes to how your catalogue's hierarchy is understood, which matters more as systems evaluate sites structurally.

The six mistakes that break Shopify schema

These account for most failed Shopify installs, and several are Shopify-specific in a way generic schema guides miss:

1. Smart quotes in the JSON. Shopify's admin code editor auto-corrects straight quotes to typographic "smart" quotes, which produces invalid JSON. A block that looks right in the editor fails to parse. Paste JSON-LD carefully and verify the quotes survived.

2. Duplicate Product emission. Covered above, the theme already emits Product, so your block creates a second one.

3. Site-wide injection of per-template schema. Putting Product schema in theme.liquid fails, because the product.* Liquid variables are unbound on pages that aren't product pages. Product schema belongs in the product template, not the global layout.

4. Missing application/ld+json MIME type. If the script tag's type attribute is wrong or absent, the block is ignored entirely.

5. Claiming entities the page doesn't support. FAQPage on a page with no visible Q&A, or Review markup on your own product page where the reviews are self-serving. This is the policy violation that risks a manual action, on Shopify as anywhere.

6. Hard-coded strings where Liquid variables belong. The classic: a developer tests with one product, hard-codes its title, and every product on the store then declares itself "Coastal Vase No. 3" because product.title was never wired in. Always use the Liquid variables, never literal values.

Implementation approaches

Three routes, with honest trade-offs:

Theme Liquid edits. Edit main-product.liquid (or your theme's product section) to output a complete JSON-LD block. Full control, and it keeps everything in the theme. The costs: it requires Liquid knowledge, and it must be re-applied or re-checked across theme updates, since a theme update can overwrite your changes.

A schema app. Several Shopify apps add structured data. Coverage varies, many are SEO-focused and thinner on identifiers, variant-level data, and the merchant listing fields. Evaluate one by installing it, generating markup for a variant product with reviews and a return policy, and running the result through the Rich Results Test. If brand, GTIN, ratings, or returns come back missing, the app isn't covering the high-value fields.

Generation from your product data. The approach that scales is generating complete markup from your live Shopify product data, pulling price, availability, identifiers from metafields, and ratings from your reviews, and keeping it in sync as products change. This is the direction we build toward: markup that stays current with the catalogue rather than a static block that drifts. Whatever you choose, the test is the same: does it produce one complete block (not a duplicate), pull from live data, and cover the merchant listing fields.

A complete Shopify Product block

Putting it together, a complete Product block for a Shopify product page, the kind that qualifies for a merchant listing rather than a basic snippet. On a real store the values come from Liquid variables and metafields, shown here as placeholders:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": "{{ shop.url }}{{ product.url }}#product",
  "name": {{ product.title | json }},
  "description": {{ product.description | strip_html | truncatewords: 60 | json }},
  "image": [
    {% for image in product.images limit: 3 %}
      {{ image | image_url: width: 1200 | prepend: 'https:' | json }}{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ],
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": {
    "@type": "Brand",
    "name": {{ product.vendor | json }}
  },
  "offers": {
    "@type": "Offer",
    "url": "{{ shop.url }}{{ product.url }}",
    "priceCurrency": {{ shop.currency | json }},
    "price": "{{ product.selected_or_first_available_variant.price | money_without_currency | strip_html }}",
    "availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}",
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>

Three things worth pointing out, because they're where Shopify implementations break:

| json is doing critical work. The Liquid json filter escapes strings correctly for JSON output. A product title containing a quote or an ampersand will break your markup without it, and product titles contain those constantly. Never interpolate a raw Liquid string into JSON-LD; always pass it through json.

gtin and mpn aren't shown because they typically live in metafields, not core product fields. Add them by referencing product.metafields.custom.gtin (or wherever you've stored them), also passed through json.

Ratings aren't shown because they come from your reviews app, not Shopify core. Add aggregateRating by referencing your app's metafields or data, and only if the rating is genuinely displayed on the page.

The money_without_currency filter is worth a specific caution: it respects the store's money format, which in some locales includes thousands separators that produce invalid JSON prices. Test the rendered output on a high-priced product, and if you see commas in the price value, use a format that outputs a clean decimal.

Reviews: the most common missing piece

Worth its own note because nearly every Shopify store has this gap.

Most stores run a reviews app, and display star ratings prominently on product pages, while emitting no aggregateRating markup at all. The reviews are visible to shoppers and invisible to search engines. That's a wasted rich result, since review stars are one of the highest-impact product enhancements.

The fix depends on your reviews app. Some inject their own aggregateRating markup (check for duplicates if so); others expose the rating data via metafields or an API that you reference in your own block. The key checks: the rating must correspond to reviews genuinely visible on the page, and you must not end up with two aggregateRating blocks from the app and your own markup both firing.

Shopify's platform constraints

A few things about Shopify shape what's possible, and knowing them saves frustration.

You don't fully control the <head>, but it doesn't matter. Shopify's theme architecture limits some low-level control, and there's a persistent myth that JSON-LD must go in the <head>. It doesn't, Google supports JSON-LD anywhere in the document, so emitting it from your product template is fine. Don't waste effort trying to force markup into the head.

Theme updates can overwrite your edits. If you modify theme Liquid directly, a theme update, or switching themes, can revert your changes. This is the strongest argument against hand-editing theme files as a long-term approach: the markup you carefully completed can silently disappear at the next update, and nobody notices until rich results vanish from Search Console. Track your customizations, or use an approach that survives theme changes.

Shopify's checkout and account pages are off-limits and irrelevant. You can't and shouldn't add product schema to checkout. Structured data belongs on your storefront pages, products, collections, blog, homepage, which is where crawlers and shoppers are anyway.

Apps stack. Every app that touches SEO or reviews may inject its own schema. A store running a theme, an SEO app, and a reviews app can have three sources emitting overlapping markup. This is why the view-source audit matters before you add anything: you may be solving a duplication problem rather than a missing-markup one.

Headless Shopify is a different situation. If you've gone headless with the Storefront API and a custom frontend (Hydrogen, Next.js, or otherwise), none of the theme's automatic schema applies, you're emitting all of it yourself. That's closer to the situation in our Next.js schema guide than to a standard Shopify store, and the same server-rendering rules apply.

The through-line: Shopify gives you a schema head-start and then makes completing it slightly awkward. Knowing where the platform helps and where it constrains you is most of what separates a store with a basic snippet from one with a full merchant listing.

Validation

Rich Results Test for product snippet and merchant listing eligibility. Test a real product page, and specifically watch for duplicate-entity warnings, on Shopify that's the signal you've layered a block on top of the theme's.

Search Console has Product snippets and Merchant listings reports showing how Google parsed markup across the whole catalogue. The "ambiguous data" warning here is the duplicate-Product problem surfacing at scale.

View source to confirm your block is present, well-formed, and, critically, not duplicated by a theme or app block you forgot about.

The Schema Markup Validator at validator.schema.org for generic syntax.

Because Shopify schema is template-driven, both its wins and its failures are catalogue-wide. Get one product template right and every product benefits; break it and every product breaks. That makes the Search Console reports the right monitoring tool.

For the full picture of what Google's structured data gallery supports beyond Product, see our complete list of what Google supports in 2026, or browse the schema types directory. If you'd rather have this generated and kept in sync automatically than maintain it in Liquid by hand, see our docs or pricing.

Frequently Asked Questions


AI Schema Gen generates complete Product schema from your live store data, brand, identifiers, ratings, shipping, and returns, as a single clean block, not a duplicate. Start free or see the ecommerce use case.

Generate perfect schema in 30 seconds

AI Schema Gen handles everything automatically, free to start.

Get Started Free