Skip to main content

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 Graphog: <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_variant picks the variant the shopper selected, or the first in-stock one; the variant.* fields describe it.
  • @context and @type declare the schema.org vocabulary and the Product type; offers holds 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.src returns a protocol-relative URL (//…), so prepend https: for an absolute URL.
  • The json filter escapes and quotes each value — don't add your own quotes around it.
  • price passes through times: 1 so it outputs as a number (33), not a string ("33").
  • Set priceCurrency to cart.currency, not shop.currency. When a store sells in multiple currencies, cart.currency reflects the currency the customer is viewing, so the markup matches the displayed price.
  • url uses the canonical_url object, the absolute URL of the current product page.
  • Output gtin only when the variant's barcode holds 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:

PropertyDescription
og:titleTitle shown in the preview
og:typeObject type — website, article, product, and so on
og:imageImage shown in the preview card
og:urlCanonical URL of the page
og:descriptionA one-to-two sentence summary
og:site_nameName of your overall store
og:localeLocale, 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:type is product on product pages and website elsewhere, keyed off template.name.
  • The default filter falls back to store-level values (shop.name, shop.description, shop.url) when the page sets none of its own.
  • The og:image width, height, and alt text help platforms render the preview card; twitter:card set to summary_large_image requests 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.