WooCommerce is the most popular e-commerce platform on WordPress, and it does output some Product schema by default — but “some” is the operative word. Out-of-the-box WooCommerce markup is often incomplete, and when you layer on Yoast SEO or Rank Math, you frequently end up with duplicate or conflicting JSON-LD blocks that confuse Google rather than helping it. Getting WooCommerce schema right requires understanding what each plugin outputs and making deliberate choices about who owns the markup.

What it does

On a WooCommerce product page, the goal is to output a single, complete Product JSON-LD block containing accurate price, availability, brand, and — if you have them — genuine aggregate ratings. The challenge is that WooCommerce core, Yoast SEO’s WooCommerce add-on, Rank Math’s e-commerce module, and any specialized schema plugins all try to own this output independently. When two of them fire on the same page, Google may see duplicate Product blocks, pick neither, or merge them incorrectly.

Key properties

Rather than list schema.org properties again (covered in detail on the product schema markup page), here is what to examine and fix in a WooCommerce context:

  • Audit for duplicate JSON-LD blocks. Use View Source or the URL Inspection tool in Google Search Console to check how many @type: Product blocks appear on a product page. More than one is a red flag. Disable schema output in all but one plugin, then verify.
  • Check what WooCommerce outputs by default. WooCommerce core adds basic product microdata (the older format) in some themes; Yoast’s free version adds a schema graph; the WooCommerce SEO add-on from Yoast adds product-specific details. Know what each one contributes before you disable anything.
  • Verify price and availability stay in sync. WooCommerce variable products with dynamic pricing or inventory management can cause the JSON-LD price to lag behind the displayed price if you’re using caching. Check that cached pages regenerate markup when stock or price changes.
  • Add aggregateRating only when WooCommerce reviews are enabled and populated. If you have zero product reviews, don’t include the property. If you do include it, the ratingValue and reviewCount must match what’s visible on the page.
  • Custom JSON-LD for gaps. If no plugin cleanly outputs what you need — brand, GTINs, category-specific properties — add a custom JSON-LD block via a plugin like WPCode or directly in your child theme’s woocommerce/single-product.php template. Keep it targeted; don’t duplicate what another plugin already handles correctly.

Example

{
  '@context': 'https://schema.org',
  '@type': 'Product',
  'name': 'Adjustable Neoprene Dumbbell Set',
  'image': 'https://example.com/wp-content/uploads/dumbbell-set.jpg',
  'sku': 'DB-ADJ-20',
  'gtin12': '012345678901',
  'brand': { '@type': 'Brand', 'name': 'IronEdge' },
  'offers': {
    '@type': 'Offer',
    'url': 'https://example.com/product/adjustable-dumbbell-set/',
    'price': '129.00',
    'priceCurrency': 'USD',
    'availability': 'https://schema.org/InStock',
    'priceValidUntil': '2026-12-31',
    'shippingDetails': {
      '@type': 'OfferShippingDetails',
      'shippingRate': {
        '@type': 'MonetaryAmount',
        'value': '0',
        'currency': 'USD'
      },
      'deliveryTime': {
        '@type': 'ShippingDeliveryTime',
        'handlingTime': { '@type': 'QuantitativeValue', 'minValue': 0, 'maxValue': 1, 'unitCode': 'DAY' },
        'transitTime': { '@type': 'QuantitativeValue', 'minValue': 3, 'maxValue': 5, 'unitCode': 'DAY' }
      }
    },
    'hasMerchantReturnPolicy': {
      '@type': 'MerchantReturnPolicy',
      'applicableCountry': 'US',
      'returnPolicyCategory': 'https://schema.org/MerchantReturnFiniteReturnWindow',
      'merchantReturnDays': 30,
      'returnMethod': 'https://schema.org/ReturnByMail'
    }
  },
  'aggregateRating': {
    '@type': 'AggregateRating',
    'ratingValue': '4.5',
    'reviewCount': '47',
    'bestRating': '5'
  }
}

Google Merchant Center and shippingDetails

Google has been pushing Product structured data closer to its Merchant Center feed requirements. The two properties that shifted from optional to strongly-recommended in 2024–2025 are shippingDetails and hasMerchantReturnPolicy. Without them, your products may still qualify for standard rich snippets, but they will not display the shipping and returns information that Google now surfaces in shopping-adjacent search results. For stores already syncing a Merchant Center product feed, the structured data and the feed should agree: if your feed says free shipping, your shippingDetails should say the same. Discrepancies can cause Google to suppress rich result eligibility or flag the listing for review.

The shippingDetails block requires a shippingDestination (a DefinedRegion) and a shippingRate (a MonetaryAmount). For free shipping to all US addresses, the region is a single DefinedRegion with addressCountry: US. For stores that ship to multiple countries with different rates, you will need one shippingDetails node per region — which is where a plugin or a custom template function becomes necessary to keep the markup in sync with WooCommerce’s shipping zones.

Adding GTIN and brand via WPCode

Most WooCommerce SEO plugins do not expose GTIN fields (gtin8, gtin12, gtin13, gtin14) directly. You can store the GTIN in a custom product field and then output it in your JSON-LD via a WPCode snippet. Here is the pattern I use — it fires on single product pages, reads a custom field called _product_gtin, and outputs a minimal supplement block that adds just the GTIN and brand without duplicating the full Product block another plugin already handles:

add_action( 'wp_head', 'ts_woo_gtin_schema' );
function ts_woo_gtin_schema() {
  if ( ! is_product() ) return;
  global $product;
  $gtin = get_post_meta( $product->get_id(), '_product_gtin', true );
  if ( ! $gtin ) return;
  $brand = get_post_meta( $product->get_id(), '_product_brand', true );
  $data = array(
    '@context' => 'https://schema.org',
    '@type'    => 'Product',
    'name'     => get_the_title(),
    'gtin12'   => $gtin,
    'brand'    => array( '@type' => 'Brand', 'name' => $brand ),
  );
  echo '<script type=&quot;application/ld+json&quot;>' . wp_json_encode( $data ) . '</script>';
}

A note on the snippet above: only use this if your main SEO plugin is not already outputting gtin and brand. If it is, you will create a second Product block, which is worse than the missing properties. Check View Source first and confirm what your current plugin outputs before adding supplementary markup.

Variable products and AggregateOffer

WooCommerce variable products — a t-shirt in three sizes, for example — present a structural problem for Product markup. The offers property on a variable product needs to use AggregateOffer instead of a single Offer, because the price differs by variation. AggregateOffer accepts lowPrice, highPrice, and offerCount. Using a single hardcoded price on a product that ranges from $29 to $89 is a policy violation — the markup does not match what users see on the page. AI Overviews that surface product pricing will pull from the structured data; if that data is stale or wrong, the surfaced information is wrong too, which erodes trust with users who click through expecting a different price.

Common mistakes

  • Running two schema plugins simultaneously without disabling overlapping output. Yoast + Rank Math both active on a WooCommerce store is one of the most common causes of duplicate Product blocks I see in audits. Pick one SEO plugin and disable schema output in the other.
  • Trusting plugin defaults without verification. Plugins do not always output complete or policy-compliant markup. Always validate with Google’s Rich Results Test and cross-check against what’s visible on the page.
  • Marking up aggregate ratings sourced from nowhere. WooCommerce has its own native review system. If it’s disabled, don’t include aggregateRating. If you’re importing ratings from a third-party system (Trustpilot, Judge.me), verify the schema reflects the real count and average from that source.
  • Ignoring variable product edge cases. On products with multiple variations, price ranges need special handling. A bare price field doesn’t work for a product that ranges from $29 to $89. Use minPrice/maxPrice inside the AggregateOffer type instead.
  • Omitting shippingDetails and hasMerchantReturnPolicy. These are now strongly recommended by Google for product rich results and required for Shopping tab eligibility. Missing them limits the rich result surface area available to your listings.

WooCommerce schema is one of those areas where a one-time audit pays for itself quickly — misconfigured markup can suppress rich results across an entire product catalog. If you want me to look at your store’s current output, start with a schema audit or get in touch about schema markup consulting. For Shopify stores facing similar challenges, see schema markup for Shopify.