Structured data mastery

Product schema for WooCommerce

What WooCommerce ships by default, what's missing for AI agents, and the filter hooks that close the gap without forking the plugin.

8 min read Updated May 1, 2026

WooCommerce ships product schema by default. It also ships an incomplete version of it — the core plugin’s JSON-LD output covers the basics that validate but not the properties that actually move AI surfacing. The fix isn’t a new plugin; it’s understanding which filter hooks the core plugin exposes and what to write at each one.

This guide walks through what WooCommerce produces out of the box, the gap between that and a complete Product schema, and the filter-hook pattern that fills the gap from a custom theme function or a small site-specific plugin.

What WooCommerce ships by default

The current WooCommerce core (verified against version 9.x in 2026) generates the following Schema.org Product markup automatically:

That covers the validation bar but misses three properties that weight heavily in AI agent surfacing: gtin13 (or gtin8), brand, and category mapped to a Google product category. The first two weren’t always required; in 2026 they’re decisive for whether an AI agent treats a product as authoritative or speculative.

The gap

The core plugin’s output, side-by-side with what AI agents actually read for:

WooCommerce core

Default JSON-LD

name + description

image + sku

offers

Documented inputs to GMC + Bing

Above + brand

Above + gtin13

Above + category

Above + countryOfOrigin

Three of those four (brand, gtin13, category) come from product data WooCommerce already stores — they just don’t make it into the auto-generated schema. The pattern below pipes them in.

The filter hook pattern

WooCommerce exposes a woocommerce_structured_data_product filter on the JSON-LD output. Hooking it lets a theme or plugin extend the schema without modifying the core plugin or maintaining a fork.

Drop this into the active theme’s functions.php (or a small site-specific plugin):

add_filter( 'woocommerce_structured_data_product', function( $markup, $product ) {
    // Brand — pulled from a product attribute named "brand" or a
    // top-level brand taxonomy if the site uses one.
    $brand_terms = wp_get_post_terms( $product->get_id(), 'product_brand', [ 'fields' => 'names' ] );
    if ( ! is_wp_error( $brand_terms ) && ! empty( $brand_terms ) ) {
        $markup['brand'] = [
            '@type' => 'Brand',
            'name'  => $brand_terms[0],
        ];
    }

    // GTIN — stored as a custom field. Use barcode if your catalog
    // populates that meta key; switch to your actual key.
    $gtin = $product->get_meta( '_gtin' );
    if ( $gtin ) {
        $markup['gtin13'] = $gtin;
    }

    // Category — Google product taxonomy path, stored as a custom field.
    $google_cat = $product->get_meta( '_google_product_category' );
    if ( $google_cat ) {
        $markup['category'] = $google_cat;
    }

    // Country of origin
    $origin = $product->get_meta( '_country_of_origin' );
    if ( $origin ) {
        $markup['countryOfOrigin'] = [
            '@type' => 'Country',
            'name'  => $origin,
        ];
    }

    return $markup;
}, 10, 2 );

The pattern is uniform: read from product meta or a taxonomy, add the property to the $markup array, return it. WooCommerce serializes the result into JSON-LD on the rendered page.

Variable products

The single biggest cause of malformed WooCommerce schema. Variable products have a parent product with multiple variations; the core plugin generates a single product-level schema block that doesn’t expose variant-specific data.

Two patterns work:

  1. Server-side variant detection. When a variant is selected via URL parameter (?attribute_pa_size=large), generate the schema for that specific variant rather than the parent.
  2. hasVariant array. Keep the parent block but add a hasVariant array enumerating each variation as a ProductGroup member with its own sku, gtin13, offers, and attributes.

The first is closer to Shopify’s variant-URL pattern; the second matches Schema.org’s preferred model for catalogs where variants differ in more than just price. Across hundreds of WooCommerce stores, neither pattern ships in the default output — both require theme work.

Q&A pairs as a sibling FAQPage block

If the catalog has product-specific Q&A content, FAQPage is its own Schema.org type — render it as a second JSON-LD block alongside the WooCommerce-generated Product block. Don’t nest one inside the other; Schema.org models them as peers.

The WooCommerce pattern uses ACF (or a similar custom-field plugin) for the Q&A list, plus a wp_head hook that emits the FAQPage block:

add_action( 'wp_head', function() {
    if ( ! is_product() ) return;

    global $product;
    $faq = get_field( 'product_faq', $product->get_id() ); // ACF repeater
    if ( empty( $faq ) ) return;

    $entities = array_map( fn( $pair ) => [
        '@type'          => 'Question',
        'name'           => $pair['question'],
        'acceptedAnswer' => [
            '@type' => 'Answer',
            'text'  => $pair['answer'],
        ],
    ], $faq );

    $schema = [
        '@context'   => 'https://schema.org',
        '@type'      => 'FAQPage',
        'mainEntity' => $entities,
    ];

    echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
} );

The product_faq field is an ACF repeater with question (text) and answer (textarea) sub-fields.

This is the rendering pattern. Note that testing in late 2025 suggests that AI agents may not consistently extract JSON-LD on direct page fetch; the value of structured Q&A markup appears to come through index and feed paths rather than agents reading the page directly.

Yoast SEO and RankMath

The two most-installed SEO plugins for WordPress both inject their own schema. By default, Yoast and RankMath publish a graph that includes WebSite, WebPage, Organization, BreadcrumbList, and (on product pages) a Product block.

The conflict: WooCommerce core also publishes Product schema. With both running, you get two Product blocks on the same page. AI agents pick one at random.

The fix: disable WooCommerce’s core schema output if you’re using Yoast or RankMath, OR disable the SEO plugin’s product schema and extend the core. The plugin documentation has filters for both directions; pick one source of truth.

// Disable WooCommerce core product schema (use SEO plugin's instead):
add_filter( 'woocommerce_structured_data_product', '__return_empty_array' );

OR (more common):

In Yoast: Settings → Search Appearance → Content Types → Products
→ "Show Product as Schema piece" → off.

Where it breaks

Three failure modes that show up in production WooCommerce stores:

The contrarian take

Most WooCommerce SEO content recommends installing a dedicated schema plugin (Schema App, RankMath, Yoast). The dedicated plugins do work, but they introduce a second source of truth that conflicts with the core plugin’s schema and can drift from the catalog data.

The simpler pattern: extend the core’s schema via the filter hook, in a small site-specific plugin (not the active theme — the theme can change). The custom plugin is 50 lines, version-controlled, and survives theme switches and WooCommerce updates. SEO plugins are fine for sitemaps and meta tags; they’re overkill for product schema when the core plugin already publishes it.

Validation

The same three tools as Shopify:

  1. Google Rich Results Test (search.google.com/test/rich-results)
  2. Schema.org Validator (validator.schema.org)
  3. Manual JSON inspection — view-source on a product page, find the JSON-LD block, pretty-print, read.

Common WooCommerce-specific errors to look for:

What to ship this week

  1. Install or write the filter hook plugin above. Map the four load-bearing properties (brand, gtin13, category, countryOfOrigin) from existing product meta.
  2. Audit for double-rendered Product blocks. Disable one source (core or SEO plugin).
  3. Validate ten products end-to-end through the Rich Results Test.
  4. Wait two weeks. Re-check ChatGPT and Perplexity for the affected product types — lift is typically visible in a 14-day window.

For a 500–2,000 SKU WooCommerce store, this is a half-day project that closes the schema gap to where the core plugin meets AI agent expectations.