0

Product variants in structured data: ProductGroup, variesBy and hasVariant

A clothing product is rarely one product. A jacket comes in five sizes and two colours, and each combination has its own price, stock level and sometimes its own photo. If the structured data on the product page describes only "the jacket", search engines have to guess which offer a shopper will actually get. Google added support for product variants to its structured data documentation, and the model is simple once you see it.

The two types involved

The parent is a ProductGroup. It holds what all variants share: the name, the brand, a description and a group identifier. Two properties carry the variant logic:

  • variesBy lists the properties that change between variants, as schema.org URLs such as https://schema.org/size and https://schema.org/color.
  • hasVariant lists the variants themselves, each one a normal Product.

Each variant is a complete Product with its own sku, its own value for every property named in variesBy, and its own offers. That is the part that fixes the guessing: the price and availability now belong to "black, size M", not to the jacket in general.

A minimal example

On a store like Wear The Light, a reflective windbreaker might come in several sizes with one print. The JSON-LD for that page looks like this:

{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "name": "Reflective circuit-print windbreaker",
  "productGroupID": "windbreaker-circuit",
  "variesBy": ["https://schema.org/size"],
  "hasVariant": [
    {
      "@type": "Product",
      "name": "Reflective circuit-print windbreaker - M",
      "sku": "windbreaker-circuit-m",
      "size": "M",
      "offers": {
        "@type": "Offer",
        "price": "0.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "name": "Reflective circuit-print windbreaker - L",
      "sku": "windbreaker-circuit-l",
      "size": "L",
      "offers": {
        "@type": "Offer",
        "price": "0.00",
        "priceCurrency": "USD",
        "availability": "https://schema.org/OutOfStock"
      }
    }
  ]
}

The prices here are placeholders; in a real template they come from the same variant record that renders the size selector, so the markup can never disagree with the page.

One page or one page per variant

Shops build variant pages in two ways, and the markup follows the URL structure.

If all variants live on one URL and a selector switches between them, put the whole ProductGroup with every variant in hasVariant on that page.

If each variant has its own URL, for example ?size=m, each page can describe its own variant as a Product and point back to the group with inProductGroupWithID, using the same value as the group's productGroupID. The group itself then needs to be described once, consistently.

Mixing the two approaches on one site is where most errors come from, so pick the one that matches how the product URLs actually work.

Things that break in practice

  • The variant values in the markup must match what the page shows. If the selector says "Large" and the markup says "L", keep them consistent across the template.
  • Out-of-stock variants should stay in the markup with OutOfStock rather than disappear, or the group looks smaller than the product really is.
  • Generate the JSON-LD on the server from the same data as the HTML. Client-side injection after a size click is easy to get wrong and easy for a crawler to miss.

Google's Rich Results Test accepts a URL or a pasted code snippet, which makes it quick to check a template before it ships to every product page.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.