Technical SEO & GEO

Schema.org JSON-LD for e-commerce: how to mark up Product, Offer, variants, breadcrumbs and FAQs

A complete JSON-LD guide so your product pages show price, stock, ratings and shipping in Google and are understood correctly by AI search: required fields, ProductGroup for variants, shipping and return policy, a Shopify Liquid example, and the most common mistakes.

Short answer

The right way to add structured data to an online store is a single JSON-LD block in the <head> of every product page. It contains a Product (name, image, description, brand, SKU/GTIN) with an Offer inside (price, currency, availability, shipping and return policy) and, if you have genuine reviews, an AggregateRating. For products with variants, use the ProductGroup + hasVariant structure Google has supported since 2024.

Next to the product block add a BreadcrumbList, and once per site an Organization or OnlineStore. FAQ markup no longer produces rich results in Google for shops, but it makes the question-and-answer structure of the page explicit for AI search. Validate everything with the Rich Results Test and the Enhancements reports in Search Console.

Why structured data is a revenue question in e-commerce

Structured data means writing the information on your page (product name, price, stock, rating) in a machine-readable form that follows the Schema.org vocabulary, so search engines and AI systems can understand it with certainty. Google’s recommended format is JSON-LD: a single <script type="application/ld+json"> block, independent of your HTML.

Correct markup on a product page produces three concrete results:

  • Rich results: price, availability, star rating, shipping cost and delivery time appear in the search result. Google documents higher click-through rates for results carrying this information; in the Rotten Tomatoes case study, pages with structured data saw a 25% higher click-through rate.
  • The Shopping tab and Merchant Center: “merchant listing” markup lets your products appear in free listings even without a feed; if you have a feed, Google cross-checks the on-page data against it.
  • AI search: Google AI Overviews, ChatGPT, Perplexity and Gemini try to extract price, stock and variant details from the visible text when they cite a product; JSON-LD removes the ambiguity. Several industry studies report that pages with structured data are cited more often in AI answers.
25%Click-through lift with structured data (Rotten Tomatoes, Google case study)
JSON-LDGoogle’s recommended format (over Microdata and RDFa)
2024Year Google added ProductGroup variant support

Two separate experiences in Google product results: product snippets and merchant listings

Since 2022 Google has split product markup into two experiences. Which one you target determines the required fields.

Product snippetMerchant listing
Who it is forReview sites, comparison pages, sellersPages that sell the product directly (with an add-to-cart button)
Requiredname plus at least one of review, aggregateRating, offersname, image, offers (with price and priceCurrency)
Recommendedimage, description, brand, sku, gtinavailability, shippingDetails, hasMerchantReturnPolicy, aggregateRating, ProductGroup for variants
OutputRating, price range, availabilityPrice, stock, shipping, returns, delivery time, Shopping tab listings

For an online store the target is the merchant listing. The rule is simple: you must sell the marked-up product on that page; marking up third-party seller prices as Offers violates the policy.

A complete Product + Offer JSON-LD example (with shipping and returns)

The block below satisfies all of Google’s merchant listing and product snippet requirements for a single-variant product. Keep every value identical to what is visible on the page; Google treats markup of invisible data as spam.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": "https://www.example-store.com/products/organic-pine-honey-450g#product",
  "name": "Organic Pine Honey 450 g",
  "image": [
    "https://cdn.example-store.com/pine-honey-1x1.jpg",
    "https://cdn.example-store.com/pine-honey-4x3.jpg",
    "https://cdn.example-store.com/pine-honey-16x9.jpg"
  ],
  "description": "Unfiltered, unheated organic pine honey from the Muğla highlands. Glass jar, 450 g.",
  "sku": "HON-PINE-450",
  "gtin13": "8691234567890",
  "brand": { "@type": "Brand", "name": "Example Honey" },
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": 4.8, "reviewCount": 127 },
  "offers": {
    "@type": "Offer",
    "url": "https://www.example-store.com/products/organic-pine-honey-450g",
    "price": 24.90,
    "priceCurrency": "EUR",
    "priceValidUntil": "2026-12-31",
    "itemCondition": "https://schema.org/NewCondition",
    "availability": "https://schema.org/InStock",
    "seller": { "@type": "Organization", "name": "Example Store" },
    "shippingDetails": {
      "@type": "OfferShippingDetails",
      "shippingRate": { "@type": "MonetaryAmount", "value": 0, "currency": "EUR" },
      "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "DE" },
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
        "transitTime":  { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 3, "unitCode": "DAY" }
      }
    },
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "DE",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 14,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn"
    }
  }
}
</script>

Three details matter: price must be a number, not text (24.90, not "€24.90"). For image, Google recommends at least 50,000 pixels in 1:1, 4:3 and 16:9 aspect ratios. availability values must be full URLs: InStock, OutOfStock, PreOrder, BackOrder.

Products with variants: ProductGroup and hasVariant

The most common mistake with size and colour variants is cramming a price range into a single Product. Since 2024 Google supports modelling variants with ProductGroup: the parent group carries the shared fields, each variant is its own Product with its own SKU, price and stock, and variesBy states which attribute changes.

{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Cotton Basic T-Shirt",
  "brand": { "@type": "Brand", "name": "Example Textiles" },
  "productGroupID": "TSH-BASIC",
  "variesBy": ["https://schema.org/size", "https://schema.org/color"],
  "hasVariant": [
    {
      "@type": "Product", "sku": "TSH-BASIC-S-BLK", "name": "Cotton Basic T-Shirt – S / Black",
      "size": "S", "color": "Black", "image": "https://cdn.example.com/tsh-blk.jpg",
      "offers": { "@type": "Offer", "price": 19.00, "priceCurrency": "EUR",
                  "availability": "https://schema.org/InStock",
                  "url": "https://www.example.com/products/basic-tshirt?variant=s-black" }
    },
    {
      "@type": "Product", "sku": "TSH-BASIC-M-BLK", "name": "Cotton Basic T-Shirt – M / Black",
      "size": "M", "color": "Black", "image": "https://cdn.example.com/tsh-blk.jpg",
      "offers": { "@type": "Offer", "price": 19.00, "priceCurrency": "EUR",
                  "availability": "https://schema.org/OutOfStock",
                  "url": "https://www.example.com/products/basic-tshirt?variant=m-black" }
    }
  ]
}

If variant URLs are parameterised versions of the same page, the canonical tag should point to the main product page; Google does not need to index each variant URL separately.

Page-level companions: BreadcrumbList, Organization/OnlineStore, WebPage

Shows the category path in the search result as a readable trail instead of a URL and clarifies your site’s information architecture. Add it to every product and category page:

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example-store.com/" },
    { "@type": "ListItem", "position": 2, "name": "Honey", "item": "https://www.example-store.com/honey/" },
    { "@type": "ListItem", "position": 3, "name": "Organic Pine Honey 450 g" }
  ]
}

Organization or OnlineStore

Once per site, ideally on the home page: name, logo, URL, contact details and social profiles (sameAs). Google supports defining a store-wide return policy once on the OnlineStore type with hasMerchantReturnPolicy, so you do not repeat it in every Offer. Recognition of your brand as an entity by AI systems also starts from this block.

A connected @graph

Put several types in one block with a @graph array and link them via @id: Product → isPartOf WebPage → isPartOf WebSite → publisher Organization. This prevents the same entity being defined twice on a page.

FAQ and HowTo: the real situation in 2026

In August 2023 Google restricted FAQPage rich results to well-known government and health sites, and in September 2023 it removed HowTo rich results entirely. FAQ markup on a shop no longer produces expandable Q&A in search results.

It is still worth adding, for two reasons: Google continues to read the markup to understand the page, and AI search (AI Overviews, ChatGPT, Perplexity) prefers to quote content that is already in question-and-answer form. The rule: questions and answers must be visible on the page exactly as marked up, with no hidden text.

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [{
    "@type": "Question",
    "name": "Does pine honey crystallise?",
    "acceptedAnswer": { "@type": "Answer",
      "text": "No. Thanks to its low glucose content, pine honey stays liquid for years at room temperature; crystallisation suggests the product may be a blend." }
  }]
}

Reviews and ratings: where they are allowed and where they are not

  • AggregateRating computed from genuine customer reviews on a product page is allowed and recommended for merchant listings.
  • Marking up ratings about your own company (Organization, LocalBusiness) that you wrote or collected on your own site counts as a “self-serving review”; Google ignores it and it carries a manual-action risk.
  • The review count must match what is visible on the page; if no scale is given, 1–5 is assumed.

Implementing on Shopify: theme snippet and conflict check

Most Shopify themes (including Dawn) output a basic Product JSON-LD on product pages. The problem is that SEO apps and review apps add their own blocks too: three different Product definitions on one page, with conflicting prices and ratings. First count how many times application/ld+json appears in the page source; if more than once, keep a single source.

To enrich the theme’s own block, use Shopify’s json filter in snippets/structured-data.liquid (or the equivalent file in your theme); writing quotes by hand produces escaping errors:

{%- if request.page_type == 'product' -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "url": {{ shop.url | append: product.url | json }},
  "image": {{ product.featured_image | image_url: width: 1200 | prepend: 'https:' | json }},
  "description": {{ product.description | strip_html | truncate: 500 | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "brand": { "@type": "Brand", "name": {{ product.vendor | json }} },
  "offers": {
    "@type": "Offer",
    "price": {{ product.selected_or_first_available_variant.price | divided_by: 100.0 | json }},
    "priceCurrency": {{ cart.currency.iso_code | json }},
    "availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
    "url": {{ shop.url | append: product.url | json }},
    "itemCondition": "https://schema.org/NewCondition"
  }
}
</script>
{%- endif -%}

For variant products, loop over product.variants with the same logic to produce a ProductGroup. Defining shipping and return policy at store level (Merchant Center or OnlineStore) is more sustainable on Shopify than writing it per product.

Validation and monitoring

  1. Rich Results Test (search.google.com/test/rich-results): shows which rich results Google considers the page eligible for and which fields are missing. Test a URL or a code snippet.
  2. Schema Markup Validator (validator.schema.org): vocabulary-level syntax check, independent of Google.
  3. Search Console → Enhancements → Product snippets / Merchant listings: the valid, warning and error distribution across all product pages. It populates 1–2 weeks after deployment; faulty URLs are listed here.
  4. Merchant Center → Products → Data quality: price and stock mismatches between the feed and on-page markup.

The 8 most common mistakes

MistakeConsequenceFix
Price written as text ("€24.90")Offer invalidA number plus a separate priceCurrency
Several Product blocks on one pageConflicting data, lost rich resultsKeep one source; disable the apps’ blocks
Marking up a rating or price not visible on the pageSpam policy violation, manual actionMark up only visible data
Product markup on a category pageIgnored by GoogleItemList on categories, Product on products
Squeezing variants into one Product with a price rangeWrong price shownProductGroup + hasVariant
Relative image URLsImage cannot be fetchedAbsolute https:// URLs
Removing markup when out of stockPage drops out of rich resultsKeep availability: OutOfStock
Marking up company ratings on OrganizationSelf-serving review, ignoredMark up product reviews only

Structured data works together with the technical health of the page: a slow product page does not get clicked even with a rich result. For speed, see the Shopify Core Web Vitals guide; for conversion measurement, the GA4 Consent Mode v2 and Meta CAPI article.

Key takeaways

  • Format JSON-LD, location <head>, one Product block per product; disable duplicate blocks generated by apps.
  • Pages that sell target merchant listings: name, image, offers.price and priceCurrency are required; availability, shipping and returns are recommended.
  • For variants use ProductGroup + hasVariant + variesBy; each variant gets its own Offer instead of a price range.
  • FAQPage and HowTo no longer produce rich results for shops (2023), but FAQ markup is valuable for AI search; it must match the visible text exactly.
  • Price as a number, images as absolute URLs in three aspect ratios, ratings only from genuine product reviews.
  • Validate with the Rich Results Test before deployment, and with the Search Console Enhancements and Merchant Center data-quality reports afterwards.

Frequently asked questions

Which schema.org types are mandatory on an e-commerce site?

None are mandatory; for Google rich results a product page needs Product with an Offer inside. For merchant listings on pages that sell, name, image, offers.price and priceCurrency are required. Adding Organization (or OnlineStore) site-wide and BreadcrumbList on every page is recommended.

Should I use JSON-LD or Microdata?

JSON-LD. Google explicitly recommends it; because it is independent of the HTML it does not break with theme changes, it can be managed through apps or a tag manager, and types can be linked with @graph. Microdata and RDFa are still supported but hard to maintain.

How should I mark up products with variants?

With the ProductGroup type. Shared fields (name, brand, description) go on the group; each variant sits inside hasVariant as a Product with its own sku, size/color, image and Offer; variesBy states which attributes change. Google has supported this structure for merchant listings since 2024.

Is FAQ schema still useful in 2026?

Google no longer shows expandable Q&A in search results for shops; since August 2023 that experience is limited to government and health sites. But the markup still helps Google understand the page, and AI search engines such as AI Overviews, ChatGPT and Perplexity quote question-and-answer content more readily. It is worth adding as long as the questions are visible on the page.

My Shopify theme already outputs JSON-LD. Should I touch it?

Check it. Dawn and its derivatives produce a basic Product block, but shipping, returns, GTIN and the variant structure are missing. More importantly, SEO and review apps add their own blocks; several Product definitions on one page cause conflicts. Count the application/ld+json occurrences in the page source and keep a single source.

Does structured data help with visibility in AI search?

Yes, indirectly but concretely. When AI systems can identify the entities on a page (product, brand, price, stock) with certainty, they cite that page with more confidence. Product, Organization and FAQPage markup works best combined with clear headings, visible Q&A and up-to-date date information.

Should I remove the markup for out-of-stock products?

No. Set availability to https://schema.org/OutOfStock and keep the page live. Removing the markup resets the page’s rich-result history; when stock returns you would have to wait for re-evaluation.

Sources

  1. Google Search Central – Product structured data (overview)
  2. Google Search Central – Merchant listing structured data
  3. Google Search Central – Product variant structured data (ProductGroup)
  4. Google Search Central – Breadcrumb structured data
  5. Google Search Central – FAQPage structured data and the 2023 change
  6. Google Search Central – Organization structured data (OnlineStore, return policy)
  7. Google Search Central – Review snippet guidelines (self-serving reviews)
  8. Google – Structured data case studies (Rotten Tomatoes, 25% CTR)
  9. Schema.org – Product
  10. Shopify.dev – json Liquid filter