LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

SEO · AEO · TECHNICAL · 5 MARCH 2026 · 8 MIN READ

Structured data for variants, bundles and configurable products

A variant has a schema.org type built for it, a bundle has one nobody reads, and a configurator has none. Knowing which of the three you are marking up decides everything else.

A search result and the page behind it

Mark up variants with ProductGroup, and nothing else. It is the schema.org type built for "a group of Products that vary only in certain well-described ways, such as by size, color, material etc.", it carries hasVariant, variesBy and productGroupID, and Shopify's own structured_data Liquid filter already emits it for any product that has variants. Bundles are a different problem: schema.org models them with ProductCollection and includesObject, but Google documents no rich result for that, so the markup is for answer engines and nothing else — and the honest choice is often to describe the bundle as an ordinary Product and put the component list in visible page copy. Configurable products, where the customer assembles something that does not exist as a SKU until they buy it, have no correct markup at all, and the right answer is to mark up the base product truthfully rather than invent a price.

IN SHORT

  • Shopify’s `structured_data` Liquid filter emits `schema.org/Product` for a product without variants and `schema.org/ProductGroup` for one with them, so the correct type is often already on the page before you install anything.
  • Google supports exactly six properties in `variesBy`: `color`, `size`, `suggestedAge`, `suggestedGender`, `material` and `pattern`. A variant axis outside that list is not a variant dimension Google will read.
  • Google requires that a site "must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)" — so a variant picker that changes nothing in the address bar breaks variant markup regardless of how correct the JSON-LD is.
  • On multi-page implementations, "each page must have full and self-contained markup for the entities defined on that page", which rules out a child page that references a parent it does not itself describe.
  • Bundles have a schema.org model — `ProductCollection` with `includesObject` pointing at `TypeAndQuantityNode` — and no documented Google rich result, so treat it as answer-engine markup rather than a search feature.
  • Merchant listing markup requires a price greater than zero, which is why a configurator with no fixed price should be marked up as the base product rather than given an invented one.

Start by finding out what your theme already emits

Most variant markup projects begin by installing something, and most of them should begin by reading the page source. Shopify ships a Liquid filter, structured_data, that "converts an object into a schema.org structured data format" and supports the product and article objects. Applied to a product it emits schema.org/Product when the product has no variants and ProductGroup when it does.

That matters because the single most common structured data fault on an ecommerce site is duplication — a theme, an SEO app and a reviews app each emitting their own Product block on the same URL — and a variant project that adds a fourth emitter to the page makes the problem worse while appearing to fix it. Inventory first. If the theme is already producing a correct ProductGroup, the work is extending it, not replacing it.

The second thing to check is whether the markup agrees with the page. Google's position on this is not a style preference: structured data must reflect content that is visible to the reader. A ProductGroup listing eight variants on a page that shows three is a mismatch no validator will catch and a manual reviewer will.

ProductGroup, and the three properties that do the work

schema.org defines a ProductGroup as "a group of Products that vary only in certain well-described ways, such as by size, color, material etc." Only name is required. Everything that makes the markup useful is in the recommended set, and three properties carry almost all of it.

  • hasVariant — "Indicates a Product that is a member of this ProductGroup (or ProductModel)." Each variant is a nested Product with its own offers, sku and image. Its inverse, isVariantOf, points the other way and is what a separate variant page uses.
  • variesBy — "Indicates the property or properties by which the variants in a ProductGroup vary." Google supports six values and only six: color, size, suggestedAge, suggestedGender, material and pattern. If your product varies by voltage, length of chain, or grind, there is no supported dimension for it — state the variation in name and description and do not force it into size.
  • productGroupID — "The identifier of the product group (also known as the parent sku)." This is the field that tells a crawler that eight URLs are eight views of one product rather than eight products. Getting it wrong, or omitting it, is how a catalogue with plenty of variant markup still reads as duplicate content.

One page or many — decide this before you write any JSON-LD

Google documents two shapes, and the rules differ enough that picking the wrong one produces markup that validates and does nothing.

Single-page. All variants live on one URL. The rule is that "there must be only one distinct canonical URL for the overall ProductGroup that all variants belong to." This is the default Shopify shape and, for most catalogues, the right one: one product, one page, one canonical, variants nested under hasVariant.

Multi-page. Each variant has its own URL. Here the rule is that "each page must have full and self-contained markup for the entities defined on that page (meaning, off-page entities shouldn't be necessary to fully understand the markup on the page itself)." A variant page that emits isVariantOf and trusts the parent to supply the name, brand and images is not compliant — it has to describe itself completely as well.

Underneath both sits a constraint that has nothing to do with markup and breaks more implementations than any property does. Google states that the site "must have the ability to preselect each variant directly with a distinct URL (using URL query parameters)... This allows Google to crawl and identify each variant." A variant picker implemented entirely in JavaScript, which swaps the price and the image without touching the address bar, cannot satisfy that. On Shopify the fix is the ?variant= parameter the platform already supports, wired so the selected variant is reflected in the URL and a direct hit on that URL preselects it. That is a front-end change, not a schema one, and it is the part worth doing first.

It is also the point where the work stops being an SEO ticket and becomes a theme ticket. If your product template cannot express variant state in a URL, that is a symptom of a template built without it in mind, and the cheaper fix is usually to sort out the template — the same work a composable [section library](/services/build/shopify-section-library) exists to make routine — rather than to bolt a schema app on top of a page that cannot support it.

Bundles are not variants, and the markup for them buys less than you think

A bundle is several distinct products sold as one purchasable thing. That is not variation, so ProductGroup is wrong for it, and schema.org has a separate model: ProductCollection, defined as "a set of products (either ProductGroups or specific variants) that are listed together e.g. in an Offer", with includesObject which "links to a node or nodes indicating the exact quantity of the products included in an Offer or ProductCollection". Each entry is a TypeAndQuantityNode — a product plus how many of it.

It is a clean model. The awkward fact is that Google publishes no rich result built on it. Marking a bundle up as ProductCollection will not change how it appears in search, so the reason to do it is comprehension by systems that read the graph rather than the feature list — AI answer engines being the obvious current one. That is a legitimate reason, and it is a much weaker one than a rich result, so size the effort accordingly.

For most bundles, the pragmatic answer is to mark the bundle up as a Product with its own price and availability, because that is what it is from the buyer's side, and to put the component list in visible page copy where both a person and a crawler can read it. If you want includesObject as well, add it — but add it because you have decided answer engines matter, not because you think it will produce a carousel.

On the Shopify side, know which kind of bundle you have before you describe it. Shopify distinguishes fixed bundles — "Standard bundles and Multipack bundles that fit within Shopify's variant limits", which behave like ordinary products on ordinary product pages — from customized bundles such as mix-and-match, which are assembled through a Cart Transform Function. Fixed bundles are straightforward to mark up because they are real SKUs with real prices. Customized ones are not, for the same reason configurators are not.

Two constraints are worth knowing before you plan around bundles at all: Shopify documents that bundles can be sold through the Online Store, Shop and Shopify POS channels plus custom storefronts with bundle publishing enabled, and that bundles "can't be sold with selling plans, such as subscriptions, pre-orders, and try-before-you-buy". If your bundle plan and your subscription plan are the same roadmap item, that is a conflict to resolve now rather than at build time.

Configurable products, where honest markup means less markup

A configurator — a made-to-measure blind, a built-to-order bike, a personalised gift with twelve inputs — produces a thing that has no SKU until the order exists. There is no ProductGroup, because the combinations are not a well-described small set of variations; there is no single price, because the price is a function.

The temptation is to invent something: a representative price, a fictional set of variants, a ProductGroup with the six combinations you happen to photograph. Do not. Merchant listing markup requires a price greater than zero, and supplying one that no buyer can actually pay is exactly the mismatch between markup and visible content that policy prohibits.

The honest markup for a configurable product is a Product describing the base item, with an offers block carrying a real starting price where one exists — the "from" price the page itself shows — and nothing invented beyond it. Where the page genuinely shows no price until configuration, an AggregateOffer with a real low and high price is defensible if those numbers come from your pricing model. If they do not, leave the offer out. A product page with accurate partial markup outranks one with complete fictional markup, because the fictional one is one review away from losing rich results across the site.

Shopify's own limits shape this too. A single product query can fetch up to 2,048 variants, and the option maximum is a per-shop resource limit rather than a fixed number in the docs — but the practical ceiling on a configurator arrives long before either, because 2,048 variants is not a product page, it is a database with a picker on top. When the combination count stops fitting the variant model, the product stops being a variant problem and the markup should stop pretending otherwise.

Combined listings, and the parent nobody marked up

Shopify's combined listings feature creates a third case that catches teams out. A combined listing is "a product type created by connecting separate products from your store into a single product listing" — child products that display as variants on the storefront while keeping their own product pages, titles, descriptions, URLs and image galleries. The limits are documented: up to 2,000 variant option values across the children, a maximum of 60 products per listing, and up to three options added at the listing level, on a Plus or enterprise plan with a compatible theme.

That structure is a multi-page variant implementation whether or not anybody planned it as one, and it fails Google's self-containment rule by default if the child pages emit only their own Product markup with no relationship expressed. The fix is to add isVariantOf and a shared productGroupID on the children, keep each child's markup complete in its own right, and make sure the parent listing is the canonical the children point at.

Get that wrong and you have built the exact thing variant markup exists to prevent: sixty near-identical pages, each independently claiming to be a product, competing with each other for the same query.

The order we would do this in

Markup last. That is the whole recommendation, and it is the opposite of how these projects usually run.

  • Inventory what is emitting structured data on a product page today. Expect more than one emitter and resolve that before adding anything.
  • Make variant selection addressable — ?variant= in the URL, direct hits preselecting correctly. Without this, none of the rest counts.
  • Classify each part of the catalogue: true variants, fixed bundles, customized bundles, configurators, combined listings. They need different markup and some need none.
  • Extend the theme's existing ProductGroup output rather than adding a second emitter, and populate productGroupID from the parent SKU you already have in your product data.
  • Check every value against the visible page. If the markup says something the page does not, the page wins and the markup comes out.

Questions this raises

Should I use ProductGroup or Product for a product with variants?

`ProductGroup`. schema.org defines it as a group of products that vary only in well-described ways, and Shopify's `structured_data` Liquid filter already emits `ProductGroup` for a product with variants and plain `Product` for one without. Check what your theme produces before deciding you need to build anything.

Can I mark up any variant dimension I like?

You can put anything in the markup; Google supports six values in `variesBy` — `color`, `size`, `suggestedAge`, `suggestedGender`, `material` and `pattern`. If your products vary by something outside that list, describe the variation in the variant `name` and `description` instead of forcing it into an unrelated property.

Do variants need their own URLs?

They need to be addressable, which is not quite the same thing. Google requires the ability to preselect each variant directly with a distinct URL using query parameters, so `?variant=` on one canonical page satisfies it. Separate indexable pages per variant are a different implementation with a stricter rule: each page must carry full, self-contained markup.

Is there a rich result for product bundles?

Not one Google documents. schema.org models bundles with `ProductCollection` and `includesObject`, and that markup is worth adding if you care about how AI answer engines understand your catalogue — but it will not change your appearance in search results, so do not budget for it as though it will.

What price should a configurable product declare?

A real one or none. Merchant listing markup requires a price greater than zero, so the options are a genuine starting price that the page also displays, an `AggregateOffer` with real low and high values from your pricing model, or no offer block. An invented price is the kind of markup-to-content mismatch that costs you rich results across the whole site.

How do Shopify combined listings affect structured data?

They create a multi-page variant structure by default — children keep their own URLs and pages while displaying as variants on the parent. Each child page needs complete markup of its own plus `isVariantOf` and a shared `productGroupID`, or you have up to 60 pages independently claiming to be separate products.

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.