Structured data
Structured data and social meta tags help search engines and social platforms understand your pages. They power rich results (such as product price and availability in search) and link previews on social media. This guide covers adding JSON-LD structured data and Open Graph tags to your theme.
Both live in a page's HTML and are read by machines, not shown to customers:
- Structured data tells a search engine what a page is about — for a product, its price, availability, and rating — so the search result can show those details inline (a rich result) instead of just a title and link. The standard format is JSON-LD: a block of JSON inside a
<script type="application/ld+json">tag, the way Google recommends providing it. - Social meta tags tell a social platform what image, title, and description to show when someone shares your link (the link preview card). The standard format is Open Graph —
og:<meta>tags, originally from Facebook and now read by most platforms.
For page titles, descriptions, and canonical URLs, see Metadata.
🚧 Caution
Structured data must match the content that's visible on the page. If the markup reports a price, currency, or availability that differs from what customers see, search engines may ignore it or flag the page.
Add website structured data
On any page, you can describe the store itself with WebSite structured data:
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "WebSite",
"name": {{ shop.name | json }},
"url": {{ shop.url | json }}
}
</script>
Add product structured data
Output Product structured data as JSON-LD inside a <script type="application/ld+json"> tag. Render it from a snippet included in the <head> of your layout, on product pages.
Select the variant to describe, then build the JSON from the product, variant, cart, and shop objects:
{% assign variant = product.selected_or_first_available_variant %}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": {{ product.title | json }},
"url": {{ canonical_url | json }},
"image": {{ product.image.src | prepend: 'https:' | json }},
{% if product.description != blank %}"description": {{ product.description | strip_html | json }},{% endif %}
{% if product.vendor != blank %}"brand": { "@type": "Brand", "name": {{ product.vendor | json }} },{% endif %}
{% if variant.sku != blank %}"sku": {{ variant.sku | json }},{% endif %}
{% if variant.barcode != blank %}"gtin": {{ variant.barcode | json }},{% endif %}
"offers": {
"@type": "Offer",
"url": {{ canonical_url | json }},
"priceCurrency": {{ cart.currency | json }},
"price": {{ variant.price | times: 1 | json }},
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/{% if variant.available %}InStock{% else %}OutOfStock{% endif %}",
"seller": { "@type": "Organization", "name": {{ shop.name | json }} }
}
}
</script>
What each part does, and the details that keep the output valid and accurate:
product.selected_or_first_available_variantpicks the variant the shopper selected, or the first in-stock one; thevariant.*fields describe it.@contextand@typedeclare the schema.org vocabulary and theProducttype;offersholds the purchasable price, item condition, stock status, and seller.- The optional fields (
description,brand,sku,gtin) are wrapped in{% if %}guards, so each is output only when it has a value — empty entries would make the JSON invalid. image.srcreturns a protocol-relative URL (//…), so prependhttps:for an absolute URL.- The
jsonfilter escapes and quotes each value — don't add your own quotes around it. pricepasses throughtimes: 1so it outputs as a number (33), not a string ("33").- Set
priceCurrencytocart.currency, notshop.currency. When a store sells in multiple currencies,cart.currencyreflects the currency the customer is viewing, so the markup matches the displayed price. urluses the canonical_url object, the absolute URL of the current product page.- Output
gtinonly when the variant'sbarcodeholds a valid GTIN (UPC, EAN, or ISBN).
Variable-price products
When a product's variants span a price range, describe it with an AggregateOffer instead of a single Offer, using price_min and price_max:
"offers": {
"@type": "AggregateOffer",
"priceCurrency": {{ cart.currency | json }},
"lowPrice": {{ product.price_min | times: 1 | json }},
"highPrice": {{ product.price_max | times: 1 | json }},
"offerCount": {{ product.variants.size }}
}
Add Open Graph and Twitter tags
Open Graph (og:) and Twitter Card meta tags control how a page looks when shared on social platforms. Add them to the <head>.
The most common Open Graph properties:
| Property | Description |
|---|---|
og:title | Title shown in the preview |
og:type | Object type — website, article, product, and so on |
og:image | Image shown in the preview card |
og:url | Canonical URL of the page |
og:description | A one-to-two sentence summary |
og:site_name | Name of your overall store |
og:locale | Locale, such as en_US |
The first four (og:title, og:type, og:image, og:url) are required by the protocol; the rest are optional. For the full set of properties and object types, see the Open Graph protocol.
For example, Set the values from the current page's objects, falling back to store-level values:
<meta property="og:type" content="{% if template.name == 'product' %}product{% else %}website{% endif %}">
<meta property="og:title" content="{{ page_title | default: shop.name | escape }}">
<meta property="og:description" content="{{ page_description | default: shop.description | strip_html | escape }}">
<meta property="og:url" content="{{ canonical_url | default: shop.url }}">
{% if product.image %}
<meta property="og:image" content="https:{{ product.image.src | img_url: '1200x' }}">
<meta property="og:image:secure_url" content="https:{{ product.image.src | img_url: '1200x' }}">
<meta property="og:image:width" content="{{ product.image.width }}">
<meta property="og:image:height" content="{{ product.image.height }}">
<meta property="og:image:alt" content="{{ product.image.alt | escape }}">
{% endif %}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ page_title | default: shop.name | escape }}">
<meta name="twitter:description" content="{{ page_description | default: shop.description | strip_html | escape }}">
How the tags resolve their values:
og:typeisproducton product pages andwebsiteelsewhere, keyed offtemplate.name.- The
defaultfilter falls back to store-level values (shop.name,shop.description,shop.url) when the page sets none of its own. - The
og:imagewidth, height, and alt text help platforms render the preview card;twitter:cardset tosummary_large_imagerequests a large preview image.
On product pages, add product-specific tags for price and availability. These use the product: namespace — a Facebook extension that applies when og:type is product, not the og: prefix. Use cart.currency so the currency matches the displayed price:
{% if template.name == 'product' %}
{% assign variant = product.selected_or_first_available_variant %}
<meta property="product:price:amount" content="{{ variant.price }}">
<meta property="product:price:currency" content="{{ cart.currency }}">
<meta property="product:availability" content="{% if variant.available %}in stock{% else %}out of stock{% endif %}">
{% endif %}
Validate your output
After adding structured data, test a live product page with Google's Rich Results Test to confirm the markup is valid and eligible for rich results.