Add schema.org Product JSON-LD to Shopify so AI shopping agents can read & recommend your products
AI shopping agents (and Google) can only recommend a product if they can parse it. Plain HTML is ambiguous; structured data is not. This guide gives you one copy-paste Liquid block that emits a valid Product + Offer object on every product page — name, price, currency, availability, brand, SKU, and image.
Time: about 5 minutes. No apps, no theme rebuild. Works on any Shopify theme that uses standard product objects (Dawn and most OS 2.0 themes).
Why this matters
When an agent or crawler hits your product page, it looks for a <script type="application/ld+json"> block. That JSON tells it, unambiguously: this is a product, here is its price, it is in stock, here is the buy URL. Without it, the agent has to guess from your HTML — and most of them simply skip products they can't confidently parse. The block below removes the guesswork.
Step 1 — Open your product template
- In your Shopify admin, go to Online Store → Themes.
- On your live theme, click ⋯ → Edit code.
- Under Sections, open
main-product.liquid(Dawn / OS 2.0). On older themes, opentemplates/product.liquidinstead.
If you'd rather keep it tidy, create snippets/product-jsonld.liquid, paste the block there, and add {% render 'product-jsonld' %} inside the product section. Either location works.
Step 2 — Paste this block
Paste it anywhere inside the product section/template (the bottom of the file is fine). It must render on the product page, where the product object exists.
{%- comment -%}
schema.org Product + Offer JSON-LD for AI shopping agents.
Renders on the product page. Uses the currently selected (or first
available) variant for price, SKU, GTIN and availability.
{%- endcomment -%}
{%- assign variant = product.selected_or_first_available_variant -%}
{%- if variant.available -%}
{%- assign availability = 'https://schema.org/InStock' -%}
{%- else -%}
{%- assign availability = 'https://schema.org/OutOfStock' -%}
{%- endif -%}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 5000 | json }},
"image": [
{%- if product.featured_image -%}
"https:{{ product.featured_image | image_url: width: 1200 }}"
{%- for img in product.images limit: 5 -%}
{%- unless img == product.featured_image -%}
,"https:{{ img | image_url: width: 1200 }}"
{%- endunless -%}
{%- endfor -%}
{%- endif -%}
],
"sku": {{ variant.sku | json }},
{%- if variant.barcode and variant.barcode != blank %}
"gtin": {{ variant.barcode | json }},
{%- endif %}
"brand": {
"@type": "Brand",
"name": {{ product.vendor | json }}
},
"offers": {
"@type": "Offer",
"url": "{{ shop.url }}{{ product.url }}?variant={{ variant.id }}",
"priceCurrency": {{ cart.currency.iso_code | json }},
"price": "{{ variant.price | divided_by: 100.0 }}",
"availability": "{{ availability }}",
"itemCondition": "https://schema.org/NewCondition"
}
}
</script>
variant.price | divided_by: 100.0 instead of a money filter?
Shopify stores prices in cents (an integer). Dividing by 100.0 yields a clean decimal like 24.99 with a dot separator — exactly what schema.org wants. The money_without_currency filter follows your store's display format, which in some locales inserts a thousands separator (e.g. 1,299.00) that makes the JSON price invalid. The cents math is locale-proof.
Step 3 — What each field does
| Field | What it is |
|---|---|
name | Product title. | json safely quotes and escapes it. |
description | Plain-text description. strip_html removes markup; truncate keeps it sane; json escapes quotes and newlines. |
image | Array of absolute CDN URLs. image_url returns a protocol-relative URL (starts with //), so we prepend https:. Featured image first, then up to 5 more. |
sku | The selected variant's SKU. Agents use it to dedupe and match across sources. |
gtin | Variant barcode (UPC/EAN/ISBN). Only emitted when present. Strongly boosts eligibility in Google Shopping and agent matching. |
brand | Your store's product.vendor as a Brand object. |
offers.url | Canonical buy URL including the variant id, so the agent links to the exact item. |
offers.priceCurrency | ISO 4217 code (e.g. USD) from the active cart currency — correct even in multi-currency stores. |
offers.price | Decimal price from variant cents. |
offers.availability | InStock or OutOfStock, driven by variant.available. |
gtin is optional but high-value: if your variants have barcodes filled in (Admin → Products → variant → Barcode), the block emits it automatically and agents will trust your listing far more. Leave the barcode empty and the field is simply skipped — still valid. For availability, this block reflects whether the variant can be purchased right now; if you sell out, the JSON flips to OutOfStock on the next page load, so agents stop recommending an item you can't ship.
Step 4 — Validate it
Save the file, open any product page, and check it two ways:
- Paste the product URL into the Google Rich Results Test. You want a green “Product snippets” result with no errors. (A “missing field
review/aggregateRating” warning is fine — those are optional.) - Paste the same URL (or the rendered JSON) into the schema.org validator to confirm the object is structurally valid.
- Quick sanity check: open the page, View source, and search for
application/ld+json— your price, currency and availability should all be filled in.
"@type": "Product" first — if one already exists and validates cleanly, you may not need this. If it's missing fields (no offers, no availability), replace it with the block above.
Did this close the gap?
Re-run your store through the free Hatchloop checker to confirm no_product_schema is gone — and see what else AI shopping agents look for (feeds, canonical pricing, crawlability).