WP Manifestindependent plugin directory
manifest / seo / xenios-woocommerce-product-schema

Xenios WooCommerce Product Schema

Outputs clean Product JSON-LD schema for WooCommerce product pages.

by Kostas Chasiotis · github.com/kostaschasiotis/xenios-woocommerce-product-schema

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/kostaschasiotis/xenios-woocommerce-product-schema/archive/refs/heads/main.zip

A lightweight WordPress plugin that outputs clean JSON-LD Product schema for WooCommerce product pages.

The plugin is intentionally focused: it outputs Product schema for simple WooCommerce products and ProductGroup schema for variable WooCommerce products. It does not try to become a full schema framework and it does not remove or modify schema generated by other plugins.


Purpose

Xenios WooCommerce Product Schema is a standalone spin-off from the broader Xenios Schema work.

Its goal is to provide a focused WooCommerce Product schema layer that is:

  • simple to install
  • predictable in output
  • friendly to site owners
  • easy for developers to extend with WordPress filters
  • compatible with Google product structured data expectations

Plugin path

Recommended plugin path:

wp-content/plugins/xenios-woocommerce-product-schema/

Main plugin file:

wp-content/plugins/xenios-woocommerce-product-schema/xenios-woocommerce-product-schema.php

Plugin header:

/**
 * Plugin Name: Xenios WooCommerce Product Schema
 * Description: Outputs clean Product JSON-LD schema for WooCommerce product pages.
 * Version: 1.3.0
 * Author: Kostas Chasiotis
 * Author URI: https://kostaschasiotis.com
 * Text Domain: xenios-woocommerce-product-schema
 */

File structure

The plugin uses a small class-based structure with a lightweight namespace autoloader.

xenios-woocommerce-product-schema/
├── xenios-woocommerce-product-schema.php
├── uninstall.php
├── includes/
│   ├── AdminNotice.php
│   ├── Plugin.php
│   ├── SchemaBuilder.php
│   ├── Admin/
│   │   └── AttributeMappingsPage.php
│   ├── Schema/
│   │   ├── AggregateRating.php
│   │   ├── Brand.php
│   │   ├── Offer.php
│   │   ├── Product.php
│   │   ├── ProductGroup.php
│   │   └── PropertyValue.php
│   └── Support/
│       ├── Attributes.php
│       ├── Gtin.php
│       ├── Images.php
│       ├── Text.php
│       └── VariationUrl.php
└── readme.md

Naming convention

Classes inside includes/Schema/ are named after the Schema.org type they build.

Schema/Product.php         -> Product
Schema/ProductGroup.php    -> ProductGroup
Schema/Offer.php           -> Offer
Schema/Brand.php           -> Brand
Schema/AggregateRating.php -> AggregateRating
Schema/PropertyValue.php   -> PropertyValue

The plugin does not use a separate VariationProduct class because Schema.org does not have a ProductVariant type. WooCommerce variations are output as normal Schema.org Product nodes inside ProductGroup.hasVariant, so that logic lives in Schema/Product.php.

Each Schema class includes a comment block with:

  • the official Schema.org URL
  • the official Schema.org description

Example:

/**
 * Product
 *
 * Schema.org reference:
 * https://schema.org/Product
 *
 * Description:
 * Any offered product or service. For example: a pair of shoes; a concert ticket;
 * the rental of a car; a haircut; or an episode of a TV show streamed online.
 */

Core behavior

The plugin outputs JSON-LD on:

wp_head

Only when the current page is a WooCommerce single product page:

is_product()

It gets the current product with:

wc_get_product(get_queried_object_id())

If WooCommerce is unavailable, the current page is not a product page, or no valid WC_Product is found, the plugin outputs nothing.

Output can also be disabled conditionally with the xenios_wc_product_schema_output_enabled filter.


Schema output strategy

Simple products

Simple WooCommerce products output Schema.org Product.

{
  "@context": "https://schema.org",
  "@type": "Product"
}

When available, the Product schema includes:

  • @id
  • name
  • url
  • description
  • sku
  • gtin, gtin8, gtin12, gtin13, or gtin14
  • image
  • brand
  • offers
  • aggregateRating

Variable products

Variable WooCommerce products output Schema.org ProductGroup.

{
  "@context": "https://schema.org",
  "@type": "ProductGroup"
}

The parent variable product becomes the ProductGroup.

Each visible WooCommerce variation becomes a nested Schema.org Product inside:

"hasVariant": []

The ProductGroup includes, when available:

  • @id
  • name
  • url
  • productGroupID
  • description
  • sku
  • gtin, gtin8, gtin12, gtin13, or gtin14
  • image
  • brand
  • aggregateRating
  • variesBy
  • hasVariant

Each variation Product includes, when available:

  • @type: Product
  • @id
  • name
  • url
  • isVariantOf
  • sku
  • gtin, gtin8, gtin12, gtin13, or gtin14
  • image
  • brand
  • mapped variation attributes
  • additionalProperty
  • offers

Final rule:

Simple product    -> Product
Variable product  -> ProductGroup
Variation         -> Product inside ProductGroup.hasVariant

Entity IDs

The plugin adds @id values to schema entities so that related nodes can reference each other clearly.

Example ProductGroup ID:

"@id": "https://example.com/product/example/#product-group"

Example variant Product ID:

"@id": "https://example.com/product/example/?attribute_pa_size=large#variant-123"

Example variation relationship:

"isVariantOf": {
  "@type": "ProductGroup",
  "@id": "https://example.com/product/example/#product-group",
  "name": "Example Product",
  "productGroupID": "456"
}

Important: variation URLs may include selected variation attributes, but isVariantOf.@id always points back to the parent ProductGroup ID.


Variation URLs

Variation Product nodes use URLs with selected attributes appended as query parameters.

Example:

/product/example/?attribute_pa_size=large&attribute_pa_color=black

This is handled by:

includes/Support/VariationUrl.php

WooCommerce variation attributes are normalized to the expected query parameter format:

pa_size  -> attribute_pa_size
pa_color -> attribute_pa_color

Variation Product.url, variation Product.@id, and variation Offer.url should use the selected variation URL.


Attribute mapping for variants

The plugin supports deterministic mapping of WooCommerce product attributes to Schema.org Product properties.

Attribute mappings are managed in:

WooCommerce -> Product Schema

The settings page lists global WooCommerce attributes and allows the site owner to map each one to a supported Schema.org Product property.

Supported mapping targets:

Do not map
color
size
material
pattern

The mapping is stored in a single WordPress option:

xenios_wc_product_schema_attribute_mappings

This option is removed on plugin uninstall.

Mapping priority

Attribute mapping follows this order:

Saved admin mapping -> default plugin mapping -> additionalProperty

Default plugin mappings:

pa_color / color / colour -> color
pa_size / size            -> size
pa_material / material    -> material
pa_pattern / pattern      -> pattern

Example saved mapping:

pa_ring-size-for-men -> size

This produces:

"size": "60"

If an attribute is not mapped, it remains as additionalProperty:

"additionalProperty": [
  {
    "@type": "PropertyValue",
    "name": "Ring Size for Men",
    "value": "60"
  }
]

variesBy

For variable products, the plugin outputs variesBy when a variation attribute can be mapped to a known Schema.org Product property.

Example:

"variesBy": [
  "https://schema.org/size",
  "https://schema.org/color"
]

The same saved attribute mappings used for variation Product properties are also used for ProductGroup.variesBy.

Example:

pa_ring-size-for-men -> size

Produces:

"variesBy": [
  "https://schema.org/size"
]

Offers

Simple products and variation Products receive an Offer when a price exists.

Offer includes:

  • @type: Offer
  • url
  • price
  • priceCurrency
  • availability
  • itemCondition
  • priceValidUntil, when available

Variable parent products intentionally do not receive a parent-level Offer or AggregateOffer. Instead, each variation receives its own offer.

Final offer rule:

Simple Product   -> Offer
ProductGroup     -> no parent Offer
Variant Product  -> Offer

This keeps pricing precise for variable products, especially when variations have different prices, availability, or sale dates.


Availability mapping

WooCommerce stock status is mapped as:

Backorder    -> https://schema.org/PreOrder
In stock     -> https://schema.org/InStock
Out of stock -> https://schema.org/OutOfStock

Price validity

The plugin adds:

"priceValidUntil"

when WooCommerce has a sale end date:

$product->get_date_on_sale_to()

The value is formatted as:

Y-m-d

GTIN support

The plugin supports WooCommerce’s native global unique ID field when available.

It checks:

$product->get_global_unique_id()

Fallback meta key:

_global_unique_id

The value is mapped by digit length:

8 digits  -> gtin8
12 digits -> gtin12
13 digits -> gtin13
14 digits -> gtin14
Other     -> gtin

Non-numeric values fall back to:

"gtin"

GTIN output can be modified with:

xenios_wc_product_schema_gtin

SKU support

The plugin uses WooCommerce SKU when available:

$product->get_sku()

For variations, if the variation has no SKU, the plugin falls back to the parent product SKU.


Brand support

WooCommerce brands are supported through product taxonomies.

The plugin checks:

product_brand
product_brands

If a brand term exists, it outputs:

"brand": {
  "@type": "Brand",
  "name": "Brand Name"
}

There is also a filter fallback:

xenios_wc_product_schema_brand

The filter may return:

  • a string brand name
  • a full Brand schema array
  • null

Images

The plugin uses:

  1. featured product image
  2. product gallery images

For variations:

  1. variation image
  2. parent product images as fallback

All images are output as full-size URLs.

Image output can be modified with:

xenios_wc_product_schema_images

Ratings

The plugin outputs aggregate ratings only, not individual Review nodes.

It uses:

$product->get_average_rating()
$product->get_review_count()

It outputs:

"aggregateRating": {
  "@type": "AggregateRating",
  "ratingValue": 5,
  "reviewCount": 10
}

Only when the rating is greater than zero and the review count is at least one.

Individual Review nodes are intentionally not generated unless reviews are visibly rendered on the product page.


Duplicate schema handling

The plugin does not remove, disable, or alter schema generated by other plugins.

This is intentional.

The site owner is responsible for disabling duplicate Product schema from SEO/schema plugins if needed.

The plugin only displays an admin notice when common SEO/schema plugins are active.

Currently detected plugins:

Rank Math SEO
Yoast SEO
SEOPress
AIOSEO

Detection is based on constants:

RANK_MATH_VERSION
WPSEO_VERSION
SEOPRESS_VERSION
AIOSEO_VERSION

Developer filters

The plugin includes WordPress filters as developer extension points.

The plugin builds its default schema first, then applies filters at the end of each relevant build step. This means future plugin functionality is inherited automatically unless a developer intentionally changes or removes it through a filter.

Filter summary

xenios_wc_product_schema_output_enabled
xenios_wc_product_schema_product
xenios_wc_product_schema_product_group
xenios_wc_product_schema_variant_product
xenios_wc_product_schema_offer
xenios_wc_product_schema_brand
xenios_wc_product_schema_images
xenios_wc_product_schema_gtin

xenios_wc_product_schema_output_enabled

Controls whether the plugin outputs schema for the current product.

Runs before schema is built.

apply_filters(
    'xenios_wc_product_schema_output_enabled',
    true,
    $product
);

Arguments:

Argument Type Description
$enabled bool Whether schema output is enabled. Default true.
$product WC_Product Current WooCommerce product.

Expected return:

bool

Example: disable schema for one product.

add_filter('xenios_wc_product_schema_output_enabled', function (bool $enabled, WC_Product $product): bool {
    return $product->get_id() !== 123;
}, 10, 2);

Example: disable schema for products in a category.

add_filter('xenios_wc_product_schema_output_enabled', function (bool $enabled, WC_Product $product): bool {
    if (has_term('private-products', 'product_cat', $product->get_id())) {
        return false;
    }

    return $enabled;
}, 10, 2);

xenios_wc_product_schema_product

Filters the final Schema.org Product array for simple products.

Runs after the default simple Product schema has been built.

apply_filters(
    'xenios_wc_product_schema_product',
    $schema,
    $product
);

Arguments:

Argument Type Description
$schema array<string, mixed> Final Product schema array.
$product WC_Product WooCommerce product.

Expected return:

array<string, mixed>

Example: add a custom property.

add_filter('xenios_wc_product_schema_product', function (array $schema, WC_Product $product): array {
    $customValue = get_post_meta($product->get_id(), '_custom_schema_value', true);

    if (is_string($customValue) && $customValue !== '') {
        $schema['additionalProperty'][] = [
            '@type' => 'PropertyValue',
            'name'  => 'Custom Value',
            'value' => $customValue,
        ];
    }

    return $schema;
}, 10, 2);

Example: add MPN manually if a shop stores it in custom meta.

add_filter('xenios_wc_product_schema_product', function (array $schema, WC_Product $product): array {
    $mpn = get_post_meta($product->get_id(), '_mpn', true);

    if (is_string($mpn) && $mpn !== '') {
        $schema['mpn'] = $mpn;
    }

    return $schema;
}, 10, 2);

xenios_wc_product_schema_product_group

Filters the final Schema.org ProductGroup array for variable products.

Runs after the default ProductGroup schema has been built, including hasVariant.

apply_filters(
    'xenios_wc_product_schema_product_group',
    $schema,
    $product
);

Arguments:

Argument Type Description
$schema array<string, mixed> Final ProductGroup schema array.
$product WC_Product_Variable WooCommerce variable product.

Expected return:

array<string, mixed>

Example: override productGroupID.

add_filter('xenios_wc_product_schema_product_group', function (array $schema, WC_Product_Variable $product): array {
    $schema['productGroupID'] = 'group-' . $product->get_id();

    return $schema;
}, 10, 2);

Example: remove aggregate rating from ProductGroup only.

add_filter('xenios_wc_product_schema_product_group', function (array $schema, WC_Product_Variable $product): array {
    unset($schema['aggregateRating']);

    return $schema;
}, 10, 2);

xenios_wc_product_schema_variant_product

Filters the final Schema.org Product array for a WooCommerce variation.

Runs after the variation Product schema has been built, including isVariantOf, mapped attributes, images, brand, GTIN, SKU, and offer.

apply_filters(
    'xenios_wc_product_schema_variant_product',
    $variant,
    $variation,
    $parent
);

Arguments:

Argument Type Description
$variant array<string, mixed> Final variation Product schema array.
$variation WC_Product_Variation WooCommerce variation.
$parent WC_Product_Variable Parent WooCommerce variable product.

Expected return:

array<string, mixed>

Example: add custom variation metadata.

add_filter(
    'xenios_wc_product_schema_variant_product',
    function (array $variant, WC_Product_Variation $variation, WC_Product_Variable $parent): array {
        $customCode = get_post_meta($variation->get_id(), '_custom_code', true);

        if (is_string($customCode) && $customCode !== '') {
            $variant['additionalProperty'][] = [
                '@type' => 'PropertyValue',
                'name'  => 'Custom Code',
                'value' => $customCode,
            ];
        }

        return $variant;
    },
    10,
    3
);

Example: remove offers from variants for a specific parent product.

add_filter(
    'xenios_wc_product_schema_variant_product',
    function (array $variant, WC_Product_Variation $variation, WC_Product_Variable $parent): array {
        if ($parent->get_id() === 123) {
            unset($variant['offers']);
        }

        return $variant;
    },
    10,
    3
);

xenios_wc_product_schema_offer

Filters the final Schema.org Offer array for simple products and variation products.

Runs after the default Offer schema has been built.

apply_filters(
    'xenios_wc_product_schema_offer',
    $offer,
    $product
);

Arguments:

Argument Type Description
$offer array<string, mixed> Final Offer schema array.
$product WC_Product WooCommerce product or variation the offer belongs to.

Expected return:

array<string, mixed>

Example: override price validity.

add_filter('xenios_wc_product_schema_offer', function (array $offer, WC_Product $product): array {
    $offer['priceValidUntil'] = '2026-12-31';

    return $offer;
}, 10, 2);

Example: add return policy manually.

add_filter('xenios_wc_product_schema_offer', function (array $offer, WC_Product $product): array {
    $offer['hasMerchantReturnPolicy'] = [
        '@type'                => 'MerchantReturnPolicy',
        'applicableCountry'    => 'GR',
        'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
        'merchantReturnDays'   => 14,
        'returnMethod'         => 'https://schema.org/ReturnByMail',
        'returnFees'           => 'https://schema.org/FreeReturn',
    ];

    return $offer;
}, 10, 2);

Example: add shipping details manually.

add_filter('xenios_wc_product_schema_offer', function (array $offer, WC_Product $product): array {
    $offer['shippingDetails'] = [
        '@type' => 'OfferShippingDetails',
        'shippingDestination' => [
            '@type' => 'DefinedRegion',
            'addressCountry' => 'GR',
        ],
        'shippingRate' => [
            '@type' => 'MonetaryAmount',
            'value' => 5,
            'currency' => get_woocommerce_currency(),
        ],
    ];

    return $offer;
}, 10, 2);

Note: shipping and return policy output is intentionally not generated automatically in v1 because WooCommerce shipping rules can be highly conditional.


xenios_wc_product_schema_brand

Filters or provides the Product brand value.

Runs after the plugin checks the supported brand taxonomies. If no brand taxonomy term is found, this filter can provide a fallback.

apply_filters(
    'xenios_wc_product_schema_brand',
    null,
    $product
);

Arguments:

Argument Type Description
$brand array<string, mixed>|string|null Current brand value. Usually null when used as fallback.
$product WC_Product WooCommerce product.

Expected return:

array<string, mixed>|string|null

Accepted return values:

  • null to output no brand
  • string brand name
  • full Brand schema array

Example: provide a brand name manually.

add_filter('xenios_wc_product_schema_brand', function ($brand, WC_Product $product) {
    return 'Custom Brand Name';
}, 10, 2);

Example: return a full Brand object.

add_filter('xenios_wc_product_schema_brand', function ($brand, WC_Product $product): array {
    return [
        '@type' => 'Brand',
        'name'  => 'Custom Brand Name',
    ];
}, 10, 2);

Example: use a custom taxonomy as brand fallback.

add_filter('xenios_wc_product_schema_brand', function ($brand, WC_Product $product) {
    $terms = get_the_terms($product->get_id(), 'designer');

    if (empty($terms) || is_wp_error($terms)) {
        return $brand;
    }

    $term = reset($terms);

    if (! $term instanceof WP_Term) {
        return $brand;
    }

    return [
        '@type' => 'Brand',
        'name'  => $term->name,
    ];
}, 10, 2);

xenios_wc_product_schema_images

Filters the product image URLs before they are added to schema.

Runs after the plugin collects the featured image and gallery images.

apply_filters(
    'xenios_wc_product_schema_images',
    $images,
    $product
);

Arguments:

Argument Type Description
$images array<int, string> Full-size image URLs.
$product WC_Product WooCommerce product or variation.

Expected return:

array<int, string>

Example: prepend a custom schema image from post meta.

add_filter('xenios_wc_product_schema_images', function (array $images, WC_Product $product): array {
    $customImage = get_post_meta($product->get_id(), '_schema_image', true);

    if (is_string($customImage) && $customImage !== '') {
        array_unshift($images, $customImage);
    }

    return array_values(array_unique($images));
}, 10, 2);

Example: remove gallery images and keep only the first image.

add_filter('xenios_wc_product_schema_images', function (array $images, WC_Product $product): array {
    return array_slice($images, 0, 1);
}, 10, 2);

xenios_wc_product_schema_gtin

Filters or provides the GTIN result before it is added to schema.

The filter receives either:

  • null, when no GTIN was found
  • an array with property and value, when a GTIN was found
apply_filters(
    'xenios_wc_product_schema_gtin',
    $gtin,
    $product
);

Arguments:

Argument Type Description
$gtin array{property: string, value: string}|null GTIN result.
$product WC_Product WooCommerce product or variation.

Expected return:

array{property: string, value: string}|null

Valid property values:

gtin
gtin8
gtin12
gtin13
gtin14

Example: provide GTIN manually from custom meta.

add_filter('xenios_wc_product_schema_gtin', function (?array $gtin, WC_Product $product): ?array {
    $customGtin = get_post_meta($product->get_id(), '_custom_gtin', true);

    if (! is_string($customGtin) || $customGtin === '') {
        return $gtin;
    }

    return [
        'property' => 'gtin13',
        'value'    => $customGtin,
    ];
}, 10, 2);

Example: remove GTIN for one product.

add_filter('xenios_wc_product_schema_gtin', function (?array $gtin, WC_Product $product): ?array {
    if ($product->get_id() === 123) {
        return null;
    }

    return $gtin;
}, 10, 2);

Public support methods used internally

The current attribute helper keeps these public methods for consistency across schema classes:

Attributes::addToVariant(array &$variant, WC_Product_Variation $variation): void
Attributes::variesBy(WC_Product_Variable $product): array
Attributes::schemaProperty(string $attributeName): ?string
Attributes::valueLabel(string $attributeName, mixed $attributeValue): string

These are internal support methods, not formal public APIs. Developers should prefer WordPress filters for customization.


Uninstall behavior

On uninstall, the plugin removes only its own option:

xenios_wc_product_schema_attribute_mappings

No product data, WooCommerce data, schema generated by other plugins, or unrelated WordPress options are modified.


Design principles

  • Keep it simple.
  • Activate and go.
  • Keep settings minimal and only when needed.
  • No destructive behavior.
  • No automatic removal of other schemas.
  • Prefer WooCommerce native data.
  • Avoid custom fields unless handled through filters.
  • Output only valid, available data.
  • Keep Google-compatible product structured data in mind.

Intentionally not implemented in v1

Parent-level ProductGroup offers

Variable parent products do not receive a parent-level Offer or AggregateOffer.

Reason: variation pricing, availability, and sale dates can differ. Variant-level offers are more precise.

Shipping and returns

Shipping and return structured data is not generated automatically in v1.

Reason: WooCommerce shipping and return rules can vary heavily by shop and may depend on destination, cart contents, weight, shipping class, carrier APIs, plugins, coupons, and other conditional logic.

For now, developers can add shippingDetails or hasMerchantReturnPolicy through:

xenios_wc_product_schema_offer

This area is deferred to v2.

MPN support

MPN is not implemented by default because WooCommerce core does not provide a universal native MPN field.

The plugin already supports:

  • WooCommerce SKU
  • WooCommerce native global unique ID / GTIN
  • GTIN fallback meta
  • GTIN filter

Developers can add MPN through:

xenios_wc_product_schema_product
xenios_wc_product_schema_variant_product

Current project status

The plugin currently supports:

Simple WooCommerce products as Product
Variable WooCommerce products as ProductGroup
WooCommerce variations as Product nodes inside ProductGroup.hasVariant
Variation URLs with selected attribute query parameters
Correct isVariantOf relationship back to the parent ProductGroup
No parent-level ProductGroup Offer by design
Native WooCommerce SKU
Native WooCommerce GTIN/global unique ID
GTIN override filter
Native WooCommerce brand taxonomies
Brand override filter
Featured and gallery images
Image override filter
Sale end date as priceValidUntil
Aggregate ratings
Duplicate schema warning notice
Admin attribute mapping page
Single stored option for attribute mappings
Uninstall cleanup for plugin-owned option
Developer filters for Product, ProductGroup, variant Product, Offer, images, GTIN, brand, and output control

Final validation checklist

Before tagging a release, test:

  • one simple product in Google Rich Results Test
  • one variable product in Google Rich Results Test
  • one simple product in Schema Markup Validator
  • one variable product in Schema Markup Validator
  • source output for duplicate Product schema from SEO/schema plugins
  • attribute mapping page save/update behavior
  • uninstall removes xenios_wc_product_schema_attribute_mappings

Releases

1 release.

Tag
Published
1.3.0 latest
Jul 8, 2026 2mo ago

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.