Skip to content
SEO & GROWTH

Magento 2 Rich Snippets: How to Set Up Product Schema That Validates

Magento 2 rich snippets, done by hand: Google's required Product properties, a JSON-LD block mapped to Magento data, and fixes for duplicate microdata.

Shane Blandford
Shane Blandford · FOUNDERPUBLISHED · 15 MIN READ

Magento 2 rich snippets come from structured data Google can read on the product page: a Product with an Offer, ideally written in JSON-LD. Valid markup makes the page eligible. Google decides whether the stars, price and stock line actually appear. Luma and Hyva stores already ship part of this markup through the theme, and a Product block added by an extension arrives with values maintained by different code. When the two disagree, for example a theme price printed with tax while the JSON-LD carries the price before tax, one page states two prices for one product.

Disclosure: OCM Labs builds SEO Suite, a Magento module that outputs this markup. Everything below works without it.

What does Google require: product snippets or merchant listings?

Build to the merchant listing requirements. Google splits Product markup into two experiences: product snippets, for pages that describe or review a product, and merchant listings, for pages where the shopper can buy it. A Magento product page is a merchant listing page. Meeting the merchant listing requirements can also make the same page eligible for product snippets, so one well-built block covers both.

The table follows Google's merchant listing documentation and its product snippet counterpart. "Not required" means Google does not list the property as required for that experience.

PRODUCT SNIPPETSMERCHANT LISTINGSSEO SUITE EMITS
NAMERequiredRequiredYes
IMAGENot requiredRequiredYes
OFFERSRequired unless review or aggregateRating is presentRequired, as Offer (not AggregateOffer)Yes, Offer
OFFERS.PRICERequired when offers is usedRequired, greater than zeroYes
OFFERS.PRICECURRENCYRecommendedRequired, ISO 4217 three-letter codeYes
REVIEWRequired unless offers or aggregateRating is presentRecommendedYes
AGGREGATERATINGRequired unless offers or review is presentRecommendedYes
BRAND, GTIN, MPN, SKU, DESCRIPTIONNot requiredRecommendedYes (gtin and mpn once mapped)
COLOR, SIZENot requiredRecommendedNo
OFFERS.AVAILABILITYRecommendedRecommendedYes
OFFERS.URLNot requiredRecommendedYes
OFFERS.HASMERCHANTRETURNPOLICYNot requiredRecommendedNo
OFFERS.ITEMCONDITIONNot requiredRecommendedNo
OFFERS.PRICEVALIDUNTILRecommendedRecommendedNo
OFFERS.SHIPPINGDETAILSNot requiredRecommendedNo

Four rules in that table trip up Magento stores. Only Offer, not AggregateOffer, qualifies for merchant listings and for the price-drop enhancement. A zero price disqualifies the page, so a free sample product cannot be a merchant listing. A priceValidUntil date in the past may suppress the snippet. And category and listing pages are not supported for Product rich results at all, so Product markup on your category template earns nothing.

What does Magento already output on a product page?

On Luma, the core product page layout sets itemscope and an itemtype pointing at Product on the <body> element, and itemprop attributes sit on the name, SKU, price and similar fields further down the page. Hyva's product templates carry Product microdata too, and Hyva also renders its own BreadcrumbList JSON-LD. What neither theme can give you is a GTIN or an MPN, because Magento has no standard attribute for either.

So Magento structured data on a stock install is real but partial. The price itemprop prints whatever the price renderer outputs for display, which can include tax, so it will not always match a JSON-LD price computed somewhere else. None of that is wrong on its own. The problem arrives when someone adds a JSON-LD Product on top: the page now describes one product twice, and two different codebases maintain the two descriptions.

Site-level types are a separate job. BreadcrumbList belongs on product and category pages, and category pages can carry breadcrumbs even though they cannot earn Product rich results. Organization and WebSite belong on the home page. The Magento SEO guide covers the on-page work that sits around this markup.

How do you add Magento 2 rich snippets as a JSON-LD block?

Use one script with one @graph, holding the Product and its BreadcrumbList side by side. The values below are illustrative, an invented jacket on example.com, but every property name is real and every format is the one Google checks: price as a plain number string with no currency symbol, an ISO 4217 currency code, availability as a full schema.org URL, and absolute image URLs.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Product",
      "@id": "https://www.example.com/trail-running-jacket.html#product",
      "name": "Trail Running Jacket",
      "sku": "TRJ-100",
      "description": "Lightweight waterproof shell with taped seams and a packable hood.",
      "image": [
        "https://www.example.com/media/catalog/product/t/r/trj-100-front.jpg",
        "https://www.example.com/media/catalog/product/t/r/trj-100-back.jpg"
      ],
      "brand": {
        "@type": "Brand",
        "name": "Example Outdoor"
      },
      "gtin": "0123456789012",
      "mpn": "EO-TRJ-100",
      "offers": {
        "@type": "Offer",
        "price": "89.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock",
        "url": "https://www.example.com/trail-running-jacket.html"
      },
      "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": "4.5",
        "bestRating": "5",
        "worstRating": "1",
        "reviewCount": 12
      },
      "review": [
        {
          "@type": "Review",
          "name": "Dry on a long wet run",
          "author": {
            "@type": "Person",
            "name": "Dana R."
          },
          "reviewRating": {
            "@type": "Rating",
            "ratingValue": "5",
            "bestRating": "5",
            "worstRating": "1"
          },
          "datePublished": "2026-09-14",
          "reviewBody": "Held up through steady rain and packs into its own pocket."
        }
      ]
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "name": "Home",
          "item": "https://www.example.com/"
        },
        {
          "@type": "ListItem",
          "position": 2,
          "name": "Jackets",
          "item": "https://www.example.com/jackets.html"
        },
        {
          "@type": "ListItem",
          "position": 3,
          "name": "Trail Running Jacket",
          "item": "https://www.example.com/trail-running-jacket.html"
        }
      ]
    }
  ]
}

Render it on the server, in the page head, as part of the HTML Magento sends. Google advises putting Product markup in the initial HTML rather than injecting it with JavaScript, and on Magento a server-rendered block is also cached with the page.

Every value in the block has a home in Magento:

MAGENTO SOURCENOTE
NAMEProduct name attributeMatch the visible product title
SKUProduct sku
DESCRIPTIONdescription attribute, falling back to short_descriptionStrip the HTML first
IMAGEBase image, then gallery imagesAbsolute media URLs only
BRANDAn attribute you choose, usually manufacturerOutput as a Brand with a name
GTINA custom attribute you createMagento has no standard GTIN attribute
MPNA custom attribute you createNo standard MPN attribute either
OFFERS.PRICEThe final price; for configurable, grouped and bundle products, the minimal final priceMust match the price the shopper sees
OFFERS.PRICECURRENCYThe store view's current currency code
OFFERS.AVAILABILITYWhether the product is salableInStock or OutOfStock at minimum
OFFERS.URLThe product URL
AGGREGATERATINGApproved Magento_Review ratings in that store viewMagento stores ratings as a percent; divide by 20 for a 1-5 value
REVIEWThe newest approved reviews, nickname as the Person authorOnly reviews visible on the page
BREADCRUMBLISTThe category path Magento resolved for the visible breadcrumbsHome, categories, then the product

Reviews are where hand-built blocks drift out of line with Google's rules. Google requires markup to reflect content the shopper can see on the page, so a rating hidden with display:none, or reviews pulled from another store view, do not belong in the block. Decide what to do with reviews that have no rating vote or no nickname; skipping them keeps every Review object complete. And if a product has no valid offer, no rating and no review, output no Product at all, because Google rejects a Product carrying none of the three.

To build it in your own theme:

  1. Create gtin and mpn product attributes, and decide which attribute holds the brand.
  2. Write a template that prints the @graph above, filling each property from the source in the table.
  3. Add that template as a block in head.additional, in your theme's Magento_Catalog/layout/catalog_product_view.xml.
  4. Run a live product URL through the Rich Results Test and clear every error before you look at the warnings.

Merchant listing extras: where does Magento keep them?

Magento has a native source for one of the four recommended Offer extras. The other three need a custom attribute, configuration-derived logic or a Merchant Center feed:

  • priceValidUntil comes from the special price end date, special_to_date. Output it only while a special price with an end date is active, and never output a past date, since Google says an expired priceValidUntil may suppress the snippet.
  • itemCondition has no native Magento attribute. Create one, or hardcode https://schema.org/NewCondition if the store sells only new goods.
  • shippingDetails has no product-level native field. You derive it from your shipping configuration, or you supply shipping through a Merchant Center feed.
  • hasMerchantReturnPolicy has no native field either. Google recommends nesting the return policy under Organization instead of repeating it on every product, and Merchant Center feeds take precedence over markup.

If you already run a Merchant Center feed, put shipping and returns there and leave them out of the product page markup. The objection is that markup costs nothing extra once the template exists. It does cost something: shipping rules change, and a template that computes shipping from configuration is one more thing to keep in step with checkout, for a value the feed overrides anyway.

Price and availability carry more risk than any of the extras. The marked-up price has to be the final price the shopper sees, after any special price or catalog price rule, in the currency the page displays. A product that is not salable should say OutOfStock, not keep its last known state from a cached template.

How should configurable products and variants be marked up?

Google supports variant markup through ProductGroup for both product snippets and merchant listings. The configurable becomes a ProductGroup with a productGroupID, variesBy names the attributes that differ (such as https://schema.org/color and https://schema.org/size), and hasVariant lists one Product per child, each with its own sku, gtin and Offer. Google's product variant documentation covers the single-page and multi-page cases. Do not use AggregateOffer to describe variants.

Build the ProductGroup when the children differ in something Google can show: price, stock or GTIN. If every child of a configurable shares the same price and none carries its own GTIN, one Product with one Offer describes the page truthfully. The case against bothering is the render cost of loading every child's data into the parent page, which is real on a configurable with hundreds of children. Variant markup is still how Google's documentation tells a red medium apart from a blue large, so a catalog that sells by size and color pays that cost.

SEO Suite does not output ProductGroup, hasVariant or variesBy. A configurable product gets one Offer at its lowest final price. If variant-level detail matters for your catalog, that part is a template you write yourself.

How do you fix duplicate Product markup from theme microdata and extension JSON-LD?

When Magento schema markup comes from two sources, the Rich Results Test lists two Product items for one URL.

Detect. View the source of a product page and search for schema.org/Product, which finds the microdata itemtype, then for application/ld+json, and open each script to look for "@type": "Product". This command counts Product microdata scopes and every JSON-LD script on one URL, so a JSON-LD count above one still needs each script opened to see which hold a Product; run it in a loop over a file of URLs to check a sample of the catalog:

curl -sL https://www.example.com/trail-running-jacket.html \
  | grep -o -e 'itemtype="[^"]*schema.org/Product"' -e 'application/ld[+]json' \
  | sort | uniq -c

Then run the URL through the Rich Results Test, which lists each item it detects. Two Product entries under merchant listings means two descriptions.

Decide. Google reads microdata and JSON-LD on the same page, so two descriptions with matching values are not the emergency. Disagreement is, and so is a second JSON-LD emitter.

WHAT THE RICH RESULTS TEST SHOWSACTION
THEME MICRODATA AND ONE JSON-LD BLOCK CARRY THE SAME NAME, PRICE AND CURRENCYTwo valid Product items with matching valuesLeave both in place
THE PRICES DISAGREE: ONE SIDE INCLUDES TAX AND THE OTHER DOES NOT, OR A JSON-LD BLOCK FROM A SECOND EXTENSION CARRIES A STALE OR BASE-CURRENCY PRICETwo Product items with different price or priceCurrency valuesFix the JSON-LD template so it prints the price and currency the shopper sees
A JSON-LD PRODUCT HAS NO PRICECURRENCYA merchant listings error on that itemFix the template that prints it
THE MICRODATA PRODUCT LACKS A REQUIRED PROPERTY SUCH AS IMAGEA separate Product item with errorsStrip the theme microdata, as below
TWO EXTENSIONS EACH PRINT A JSON-LD PRODUCTTwo JSON-LD Product items, even with matching valuesKeep one emitter and turn Product output off in the other

Fix. Run exactly one JSON-LD emitter: turn off Product output in one extension, or remove it. Leave the theme microdata alone unless the Rich Results Test shows it as a separate item with errors, because a template override is code you then carry through every upgrade.

When you do strip it on Luma, the body-attribute removal below is the whole fix, because removing the Product scope dissolves the item. Leftover itemprop attributes come from layout arguments in core's catalog_product_view.xml (name, SKU and description), from price/amount/default.phtml, from the product gallery image markup in product/view/gallery.phtml, and from the review summary and review list microdata in Magento_Review's helper/summary.phtml and product/view/list.phtml. You only need to override those if you want the attributes gone entirely. On Hyva, check breadcrumbs as well, since Hyva's own BreadcrumbList JSON-LD plus another one is the same problem one type over.

On Luma, core's catalog_product_view.xml sets the Product scope on <body>, and an empty redeclaration removes it. Redeclare the body attributes itemtype and itemscope with an empty value, which Magento's page config treats as a removal. In your theme's Magento_Catalog/layout/catalog_product_view.xml:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <attribute name="itemtype" value=""/>
        <attribute name="itemscope" value=""/>
    </body>
</page>

Deploy it, flush the cache and view the source again: the <body> tag should carry no itemscope or itemtype.

Does any of this change between Luma and Hyva, or across Magento versions?

On Hyva the layout mechanism is the same as on Luma, but Hyva already outputs BreadcrumbList JSON-LD, so a second breadcrumb emitter means switching one off. SEO Suite handles that through its Hyva companion package, ocmlabs/module-seo-hyva, which turns Hyva's breadcrumb JSON-LD off while SEO Suite's structured data is on and hands it back when that output is off.

Google's property requirements do not depend on your Magento version. The statements about SEO Suite apply to the module's supported range: Magento Open Source and Adobe Commerce on versions 2.4.7 to 2.4.9, PHP 8.2 to 8.4, with Adobe Commerce support through ocmlabs/module-seo-commerce. The SEO Suite installation docs list both companion packages.

How do you validate and monitor product rich results?

Test one product of each type, simple, configurable and bundle, on each theme and store view you run. Then work through four checks:

  1. Run the live URL through the Rich Results Test. Fix errors first, then the warnings on recommended properties you can actually fill. The test checks syntax and required properties; it cannot tell you whether the markup breaks Google's guidelines, such as rating markup for reviews nobody can see.
  2. Use URL Inspection in Search Console to confirm Google can fetch the page and sees the markup.
  3. Watch the Merchant listings and Product snippets reports in Search Console over the following days, since they show valid and invalid items across the whole site.
  4. Check the Unparsable structured data report, which catches syntax errors such as a template that printed an empty value and left a stray comma.

Allow days, not hours, for Google to crawl and reprocess the pages. Valid markup makes a page eligible for a rich result. Whether one shows is Google's decision, and no tool or module changes that. Structured data is one area of a full store audit, and the Magento SEO checklist covers the rest.

How SEO Suite outputs this markup

SEO Suite writes one application/ld+json script into the head of every page, as a single @graph under the schema.org context. It is rendered on the server and stored in the full page cache with the rest of the HTML, so it is in the initial HTML Google fetches. On a product page the graph holds a Product and a BreadcrumbList shaped like the block above; category pages get the BreadcrumbList; the home page gets Organization (name, URL, logo and profile URLs) and a WebSite with a SearchAction pointing at the catalog search. That SearchAction describes the site search and nothing more.

Brand, GTIN and MPN come from attributes you map by attribute code, not label, under Stores > Configuration > OCM Labs > SEO > Structured Data. Brand defaults to manufacturer, and GTIN and MPN stay out of the block until you map them. Changes to those settings need a full page cache flush. A product with no valid offer, rating or review gets no Product node, and review data covers up to the five newest approved reviews.

The 58-second demo shows the Product and BreadcrumbList output on a product page, with brand, GTIN, MPN, offer and review data, then Organization and WebSite on the home page.

Video, 0:58: SEO Suite outputs Product and BreadcrumbList JSON-LD

What the module does not do:

  • It does not remove theme microdata. Luma's and Hyva's Product microdata stays alongside the JSON-LD.
  • It does not output ProductGroup, hasVariant or variesBy, and it never uses AggregateOffer.
  • It does not output shippingDetails, hasMerchantReturnPolicy, itemCondition or priceValidUntil.
  • It sets availability to InStock or OutOfStock only, never BackOrder or PreOrder.
  • It does not output FAQPage markup, which comes from the OCM Labs FAQ module (FAQ module docs).
  • It shows no Search Console data inside Magento.
  • It cannot make Google show a rich result, and it predicts nothing about rankings.

If another extension already outputs Product or Organization JSON-LD, turn off Enable JSON-LD in one of them, or retire the other, before running both. When you are weighing which one to keep, the guide to what to check in a Magento SEO extension covers the questions to ask. The full settings table is in the structured data documentation.

A hand-built block is one more template to keep in step with your theme, your price rules and every Magento upgrade. If you would rather not maintain it, SEO Suite outputs the Product, Offer, review and BreadcrumbList markup shown here, without the variant and merchant listing extras. It costs $250 for Magento Open Source and $500 for Adobe Commerce.

FAQ

What is the difference between schema markup and structured data?+
Structured data is any machine-readable description of what a page contains. Schema markup is structured data that uses the schema.org vocabulary, which is the vocabulary Google reads for Product, Offer and Review. You can write it as JSON-LD, microdata or RDFa. For a product page the two terms describe the same thing, and the format choice matters more than the name.
What is an example of product schema markup?+
The JSON-LD block earlier in this post is a full example: a Product with name, SKU, images, brand, GTIN, MPN, an Offer, an aggregate rating and one review, plus a BreadcrumbList. The smallest version that meets merchant listing requirements needs only name, image and an Offer with price and priceCurrency, but the recommended properties are what make the listing useful.
Does Magento add product structured data by default?+
Yes, partly. Luma and Hyva product templates carry schema.org Product microdata, and Hyva adds BreadcrumbList JSON-LD. There is no Product JSON-LD by default, and no GTIN or MPN, because Magento has no standard attribute for either. Run one of your product pages through the Rich Results Test to see which required and recommended properties your theme's markup is missing.
Why is Google not showing rich snippets when my markup is valid?+
Because valid markup only makes the page eligible, and Google decides whether to show the rich result. Allow days for Google to crawl and reprocess the page, confirm with URL Inspection that it sees the markup, and check the Merchant listings and Product snippets reports. The Rich Results Test also cannot catch guideline problems, such as marking up reviews shoppers cannot see.
Should I use JSON-LD or microdata on Magento?+
Use JSON-LD. Google supports JSON-LD, microdata and RDFa, and recommends JSON-LD. On Magento it also keeps the whole description in one template instead of spreading itemprop attributes across price, title and review templates, which makes it far easier to keep complete. The theme microdata can stay as long as its values agree with the JSON-LD.
Do rich snippets help SEO or rankings?+
Google's product documentation does not say they do. It describes markup as what makes a page eligible for a richer presentation in results, and it states that rich results are never guaranteed. Treat the work as presentation and data accuracy, and judge it in the Search Console reports rather than in rank tracking.
Shane Blandford

Shane Blandford

FOUNDER

Founder of Orange Collar Media, a Denver ecommerce agency behind 800+ Magento and Shopify builds. In Magento since version 1.x - building, rescuing, and supporting revenue-critical stores since 2008.

All posts by Shane Blandford→

KEEP READING

Related from the Journal.

Your store can sell more. Let's find out how much.

Start a project