WooCommerce Schema Markup: What Woo Emits, What's Missing, and How to Fix It
WooCommerce emits Product schema without any plugin, and we captured exactly what. Variable products output AggregateOffer, which disqualifies them from merchant listings, and one popular schema plugin deletes the markup entirely.
Most WooCommerce schema advice starts by telling you to install a plugin. That advice is usually wrong, and occasionally destructive.
WooCommerce already outputs Product structured data. It has done for years, it needs no configuration, and on a default install it's mostly correct. The real questions are what it leaves out, where it quietly fails Google's requirements, and which plugins break it when you add them on top.
We tested all three on a clean install. Here's exactly what WooCommerce puts in your page source. For the WordPress side of this, meaning posts, pages and everything that isn't a product, see our guide to adding schema markup in WordPress.
The Test Setup
| WordPress | 7.0.2 |
|---|---|
| WooCommerce | 10.9.4 |
| PHP | 8.4.21 |
| Theme | WordPress default |
| Test date | 30 July 2026 |
Two products: a simple product (SKU TW-001, price 29.99, in stock, featured image) and a variable product with two variations at different price points. JSON-LD extracted from the rendered front-end HTML.
What WooCommerce Outputs With No Plugin at All
On a simple product page, WooCommerce 10.9.4 emits a single JSON-LD block containing a BreadcrumbList and a Product:
{
"@type": "Product",
"@id": "https://example.com/product/test-widget/#product",
"name": "Test Widget",
"url": "https://example.com/product/test-widget/",
"description": "A controlled test product.",
"image": "https://example.com/wp-content/uploads/featured.png",
"sku": "TW-001",
"offers": [{
"@type": "Offer",
"price": "29.99",
"priceCurrency": "USD",
"priceValidUntil": "2027-12-31",
"availability": "https://schema.org/InStock",
"url": "https://example.com/product/test-widget/",
"seller": { "@type": "Organization", "name": "Schema Bench" }
}]
}
That covers all four of Google's required merchant listing properties, meaning name, image, offers.price and offers.priceCurrency, plus availability, sku, url and description. For a plugin you didn't install and didn't configure, that's a genuinely good starting position.
Add a single product review and WooCommerce automatically extends it, adding AggregateRating, a Review node and a Person author, going from 7 typed nodes to 11. No plugin, no settings.
What's Missing, and Why It Costs You
Google splits Product structured data into two experiences: product snippets (rating stars, price in the organic result) and merchant listing experiences (the richer shopping treatments). WooCommerce's native output qualifies for the first. For the second, here's the gap:
| Property | Google's status | WooCommerce native |
|---|---|---|
name | Required | ✓ |
image | Required | ✓ |
offers.price | Required | ✓ |
offers.priceCurrency | Required | ✓ |
availability | Recommended | ✓ |
sku | Recommended | ✓ (see warning below) |
aggregateRating | Recommended | ✓ once reviews exist |
description | Recommended | ✓ |
brand.name | Recommended | ✗ |
gtin / gtin13 / mpn | Recommended | ✗ |
itemCondition | Recommended | ✗ |
shippingDetails | Recommended | ✗ |
hasMerchantReturnPolicy | Recommended | ✗ |
color / size / material | Recommended | ✗ |
The five in bold are the ones that matter commercially. brand and gtin are how Google matches your listing to the same product sold elsewhere; without them you're an unidentified SKU rather than a known product. shippingDetails and hasMerchantReturnPolicy feed the shipping and returns annotations that appear directly in shopping results, and they are among the few structured data properties that visibly change what a shopper sees before they click.
The Variable Product Problem
This is the finding that matters most, and almost nobody covers it.
On a variable product with two variations, WooCommerce emits this:
"offers": [{
"@type": "AggregateOffer",
"lowPrice": "19.99",
"highPrice": "39.99",
"offerCount": 2,
"priceValidUntil": "2027-12-31",
"availability": "https://schema.org/InStock",
"priceCurrency": "USD"
}]
An AggregateOffer, not an Offer.
Google's merchant listing documentation specifically requires an Offer object. AggregateOffer is accepted for product snippets, but it does not qualify a page for merchant listing experiences. Which means:
The fix is to output per-variation Offer objects, each with its own price, availability, SKU and ideally GTIN. That means either a plugin that handles variations properly, a product feed, or custom code.
The other variable-product trap: the SKU fallback
Look at the variable product output again:
"sku": 24
That's a number, not a string. It's the WordPress post ID. When no SKU is set on the parent product, WooCommerce substitutes the numeric post ID as the SKU. Google will happily ingest it, and you end up with a product identified across the web by an internal database key that changes if you ever migrate the store.
Set real SKUs on parent products. It takes ten minutes and it's the cheapest fix on this page.
"Structured Data Missing Name" and Other WooCommerce Errors
If you've searched for this error, here's what actually causes it in WooCommerce, in order of frequency:
| Error | Real cause | Fix |
|---|---|---|
Missing field name | The Product node is being generated by a theme or plugin that reads a custom title field, not post_title, or two plugins are emitting competing Product nodes and one is incomplete | Find which output is incomplete; disable the duplicate |
Missing field image | No featured image on the product | Set one; WooCommerce reads only the featured image for the image property |
Invalid price value | Price stored with a currency symbol or thousands separator, usually after a CSV import | Store the raw numeric value in _price / _regular_price |
Missing priceCurrency | Currency not set at the store level, or a multi-currency plugin intercepting the value | Check WooCommerce → Settings → General |
Missing aggregateRating | No approved reviews yet | Not an error. Google reports it as a warning; ignore it until you have reviews |
| No items detected at all | Another plugin removed WooCommerce's output, see the next section | Identify and reconfigure the plugin |
That last row is the one people never diagnose correctly. For validation errors beyond WooCommerce specifically, see our schema errors guide.
What Happens When You Add a Schema Plugin on Top
We tested WooCommerce alongside each major SEO and schema plugin, on the same product page:
| Setup | JSON-LD blocks | Product nodes | BreadcrumbList nodes | Result |
|---|---|---|---|---|
| WooCommerce alone | 1 | 1 | 1 | Clean baseline |
| Woo + Rank Math | 1 | 1 | 0 | Cleanest, replaces Woo's output entirely |
| Woo + Yoast SEO | 2 | 1 | 2 | Duplicate breadcrumb trails |
| Woo + All in One SEO | 2 | 1 | 2 | Duplicate breadcrumb trails |
| Woo + Slim SEO | 2 | 1 | 2 | Duplicate breadcrumb trails |
| Woo + Schema & Structured Data for WP & AMP | 0 | 0 | 0 | All structured data deleted |
The duplicate breadcrumb issue
Yoast, All in One SEO and Slim SEO all defer to WooCommerce for the Product node, which is sensible, but they each add their own BreadcrumbList without removing Woo's. The result is two breadcrumb trails describing the same page in two separate JSON-LD blocks with different @id values.
It's not fatal, and Google generally picks one. But it's noise, it inflates your HTML, and it's exactly the sort of ambiguity that makes debugging a rich result problem harder than it needs to be.
The one that deletes everything
Install Schema & Structured Data for WP & AMP (SASWP) on a WooCommerce store and your product pages end up with zero structured data. Not reduced. Zero.
The cause is in output/class-saswp-output-compatibility.php:
remove_action( 'wp_footer', array( WC()->structured_data, 'output_structured_data' ), 10 );
// This removes structured data from all frontend pages
SASWP strips WooCommerce's output on the assumption that you'll configure a SASWP Product schema to replace it. If you don't, and nothing in the interface prompts you to, every product page silently loses its rich result eligibility. There's no admin notice and no error. The Rich Results Test just reports "no items detected", which most people read as a Google problem rather than a plugin problem.
Adding the Missing Properties
Option 1: A plugin that handles it
Rank Math produced the cleanest WooCommerce output in our tests and adds brand and GTIN fields to the product editor. Most product feed plugins (for Google Merchant Center) also write the identifiers back into the page markup. The full plugin-by-plugin results are in our tested comparison of WordPress schema plugins.
Option 2: Filter WooCommerce's own output
WooCommerce exposes its structured data through a filter, so you can extend rather than replace it. Add this to a child theme's functions.php or a code snippet plugin, the same approach we cover for posts and pages in adding schema markup without a plugin:
add_filter( 'woocommerce_structured_data_product', function ( $markup, $product ) {
// Brand: reads a custom field; swap for your own source.
$brand = get_post_meta( $product->get_id(), '_brand', true );
if ( $brand ) {
$markup['brand'] = array(
'@type' => 'Brand',
'name' => $brand,
);
}
// GTIN
$gtin = get_post_meta( $product->get_id(), '_gtin13', true );
if ( $gtin ) {
$markup['gtin13'] = $gtin;
}
// Condition
$markup['itemCondition'] = 'https://schema.org/NewCondition';
// Return policy: applies store-wide.
$markup['offers'][0]['hasMerchantReturnPolicy'] = array(
'@type' => 'MerchantReturnPolicy',
'applicableCountry' => 'GB',
'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
'merchantReturnDays' => 30,
'returnMethod' => 'https://schema.org/ReturnByMail',
'returnFees' => 'https://schema.org/FreeReturn',
);
// Shipping
$markup['offers'][0]['shippingDetails'] = array(
'@type' => 'OfferShippingDetails',
'shippingRate' => array(
'@type' => 'MonetaryAmount',
'value' => 0,
'currency' => get_woocommerce_currency(),
),
'shippingDestination' => array(
'@type' => 'DefinedRegion',
'addressCountry' => 'GB',
),
'deliveryTime' => array(
'@type' => 'ShippingDeliveryTime',
'handlingTime' => array( '@type' => 'QuantitativeValue', 'minValue' => 0, 'maxValue' => 1, 'unitCode' => 'DAY' ),
'transitTime' => array( '@type' => 'QuantitativeValue', 'minValue' => 1, 'maxValue' => 3, 'unitCode' => 'DAY' ),
),
);
return $markup;
}, 10, 2 );
Two things to be careful about. Every value you add must be true and match what's on the page. Inventing a free returns policy in markup you don't honour on-site is a manual action risk, not a growth hack. And hasMerchantReturnPolicy and shippingDetails sit inside the Offer, not on the Product.
Option 3: Fix variable products properly
To make variable products eligible for merchant listings, you need real Offer objects rather than an AggregateOffer. The same filter gives you the hook:
add_filter( 'woocommerce_structured_data_product', function ( $markup, $product ) {
if ( ! $product->is_type( 'variable' ) ) {
return $markup;
}
$offers = array();
foreach ( $product->get_children() as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( ! $variation || ! $variation->get_price() ) {
continue;
}
$offers[] = array(
'@type' => 'Offer',
'sku' => $variation->get_sku(),
'price' => wc_format_decimal( $variation->get_price(), wc_get_price_decimals() ),
'priceCurrency' => get_woocommerce_currency(),
'availability' => 'https://schema.org/' . ( $variation->is_in_stock() ? 'InStock' : 'OutOfStock' ),
'url' => $variation->get_permalink(),
);
}
if ( $offers ) {
$markup['offers'] = $offers;
}
return $markup;
}, 20, 2 );
Run this on a staging site first, and validate before and after. On a large catalogue, the extra markup adds real page weight: a 40-variation product produces 40 Offer objects.
A 10-Minute WooCommerce Schema Audit
- Open a simple product in Google's Rich Results Test. You should see one Product with valid Offer. If you see nothing, a plugin has removed Woo's output.
- Open a variable product. If
offersis anAggregateOffer, that product can't earn merchant listing results. - View source and count
ld+jsonblocks. More than one usually means duplicate breadcrumbs, which is untidy but survivable. TwoProductnodes is not. - Check the
skuvalue. If it's a bare number matching the post ID, the product has no real SKU. - Check for
brandand a GTIN. Almost certainly absent. This is the highest-value addition for most stores. - Confirm your prices are numeric strings, with no currency symbols and no commas.
- Check for leftover FAQPage markup. Product and category pages picked up a lot of FAQ blocks in 2021 and 2022. Google stopped showing FAQ rich results in May 2026, so that markup is now inert, and worth stripping wherever it doesn't match visible content.
FAQ: WooCommerce Schema Markup
Does WooCommerce add schema markup automatically?
Yes. WooCommerce 10.9.4 emits a Product node with name, image, sku, description, offers (price, currency, availability, priceValidUntil, seller) and a BreadcrumbList, with no plugin and no configuration. It adds AggregateRating and Review automatically once a product has approved reviews.
Do I need a schema plugin for WooCommerce?
Not for basic Product markup, since Woo covers it. You need one (or custom code) to add brand, gtin, itemCondition, shippingDetails and hasMerchantReturnPolicy, and to fix variable products. See our tested comparison of WordPress schema plugins.
Why does my WooCommerce product show no structured data?
The most common cause is another plugin removing it. Schema & Structured Data for WP & AMP does this explicitly. Deactivate schema plugins one at a time and re-test.
What does "structured data missing name" mean in WooCommerce?
Usually that a second, incomplete Product node is being emitted by a theme or plugin alongside WooCommerce's. Check how many ld+json blocks contain a Product @type; there should be exactly one.
Are variable products handled correctly?
Not for merchant listings. WooCommerce outputs AggregateOffer with lowPrice and highPrice, and Google's merchant listing documentation requires an Offer object. You need per-variation Offers.
Should I add Review schema to WooCommerce products myself?
No. WooCommerce generates Review and AggregateRating from real approved reviews automatically. Hand-writing review markup that doesn't correspond to reviews visible on the page is a policy violation.
Testing conducted 30 July 2026 on WordPress 7.0.2 / WooCommerce 10.9.4 / PHP 8.4.21. Structured data extracted from rendered front-end HTML. Google property requirements taken from the merchant listing documentation as of the same date.
Auditing Structured Data Across a Catalogue?
Use our Schema Gap Analyzer to see which product templates are missing which properties, or book a consultation for an implementation plan.