Skip to content

Schema.org structured data

Polski for WooCommerce does not print structured data of its own. WooCommerce already emits a Product JSON-LD graph on every product page, and the plugin adds fields to that graph through the woocommerce_structured_data_product filter.

That is a deliberate choice: two Product graphs on one page is a duplication error, and search engines treat it as one. So everything below is an addition to WooCommerce’s output, never a replacement for it.

Source Fields
WooCommerce core name, description, image, sku, offers with price, currency, availability and condition, aggregateRating and review
Polski for WooCommerce brand, manufacturer, gtin8 / gtin12 / gtin13 / gtin14, additionalProperty with the unit price, shippingDetails, nutrition
Polski for WooCommerce, on the offer priceValidUntil, and the Omnibus lowest 30-day price as a priceSpecification

If a product page carries no WooCommerce structured data, there is nothing to add to and the plugin adds nothing. This matters when an SEO plugin replaces WooCommerce’s graph with its own, see SEO plugins below.

The additions in the first block are gated twice: the Structured Data (Schema.org) module has to be enabled, and the Enable structured data integration switch inside it has to be on. Turning either off leaves WooCommerce’s own graph untouched and complete.

Setting Option key Default Controls
Enable structured data integration polski_seo[schema_enabled] on Everything in the table below
Include Brand polski_seo[schema_brand] on brand
Include Manufacturer polski_seo[schema_manufacturer] on manufacturer
Include barcodes (GTIN) polski_seo[schema_gtin] on gtin8 to gtin14
Include Unit Price polski_seo[schema_unit_price] on additionalProperty

Delivery time and nutrition have no switch of their own. They appear whenever the product has the data, and disappear when it does not.

The two offer additions are separate. priceValidUntil is written regardless of the module, because an Offer without it loses rich-result eligibility and there is no reason a shop would want that. The Omnibus price specification follows the Omnibus module and only appears while the product is actually on sale.

{
"brand": { "@type": "Brand", "name": "MyBrand" },
"manufacturer": {
"@type": "Organization",
"name": "Producer XYZ Sp. z o.o.",
"contactPoint": {
"@type": "ContactPoint",
"contactType": "product safety",
"description": "safety@xyz.pl, +48 22 000 00 00"
}
}
}

brand is the first brand found for the product. manufacturer starts as the manufacturer name from the product panel, and a GPSR manufacturer name on the product replaces it, adding the GPSR contact as a product-safety contactPoint. The address is not published; the contact string is printed as you typed it.

The barcode from the product panel is published under the key that matches its length: 8 digits become gtin8, 12 gtin12, 13 gtin13, 14 gtin14. Any other length is published as plain gtin, which is valid Schema.org but tells Google less.

Schema.org has no unit-price property, so the figure rides in additionalProperty:

{
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "Unit price",
"value": "8.90 / 100 g"
}
]
}

When the product resolves a delivery time, an OfferShippingDetails block is added:

{
"shippingDetails": {
"@type": "OfferShippingDetails",
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
"transitTime": { "@type": "QuantitativeValue", "minValue": 1, "maxValue": 3, "unitCode": "DAY" }
},
"shippingDestination": { "@type": "DefinedRegion", "addressCountry": "PL" }
}
}

Two limits worth knowing before you rely on it. The handling time is fixed at 0 to 1 days rather than read from your settings, and the transit maximum is taken from the digits in your delivery-time text, falling back to 5 days when the text has none. A delivery time written as “2 to 3 business days” therefore yields a transit maximum of 23. Keep the text to a single number if the figure matters to you.

The destination is Poland.

Products with nutrition data from the food module get a NutritionInformation object:

Stored nutrient Schema.org field
Energy (kcal) calories
Fat fatContent
of which saturates saturatedFatContent
Carbohydrate carbohydrateContent
of which sugars sugarContent
Protein proteinContent
Fibre fiberContent
Salt sodiumContent, converted

The last row is the one to read twice. Annex XV of Regulation 1169/2011 declares salt, while Schema.org only offers sodiumContent, and the two are not the same number: salt is sodium multiplied by 2.5. The value is divided by 2.5 before publishing, so a product labelled 2.5 g of salt publishes 1 g of sodium. Before 1.36.7 the figure was copied across unchanged, which published every food product as two and a half times as salty as its own label.

Energy in kJ has no Schema.org equivalent and is not published. It still appears in the storefront table, where the regulation requires it.

{
"offers": [
{
"priceValidUntil": "2026-12-31",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"priceType": "https://schema.org/MinimumPrice",
"name": "Lowest price in the last 30 days",
"price": "79.00",
"priceCurrency": "PLN"
}
}
]
}

priceValidUntil uses the sale end date when the product has one, and one year from today when it does not. It is only written when WooCommerce left the field empty.

The priceSpecification carries the Omnibus lowest price of the previous 30 days. It is the same number shown to shoppers under the price, published in a form a search engine or an assistant can quote, and it appears only while the product is on sale and the Omnibus module is on.

The added fields are cached per product in a polski_schema_<id> transient for 12 hours and the transient is deleted whenever the product is saved.

One consequence: changing the switches above does not clear the cache. An existing product keeps its previous fields for up to 12 hours, or until it is saved. Save the product to see a settings change immediately.

The plugin exposes no filters of its own for structured data. Use WooCommerce’s filter, at a priority above 10 so that your callback runs after the plugin’s:

add_filter( 'woocommerce_structured_data_product', function ( array $markup, WC_Product $product ): array {
$markup['award'] = 'Product of the Year 2025';
if ( isset( $markup['offers'][0] ) ) {
$markup['offers'][0]['warranty'] = [
'@type' => 'WarrantyPromise',
'durationOfWarranty' => [
'@type' => 'QuantitativeValue',
'value' => 24,
'unitCode' => 'MON',
],
];
}
return $markup;
}, 20, 2 );

To drop a field the plugin added, unset it in the same callback. To stop the plugin adding anything, use the switches rather than code.

There is no detection code and no per-plugin behaviour. Everything depends on what the SEO plugin does with WooCommerce’s own graph:

  • It leaves WooCommerce’s structured data in place. The additions appear as described.
  • It replaces WooCommerce’s structured data with its own graph. Nothing is added, because the filter never runs. Several SEO plugins offer a switch for exactly this; re-enable WooCommerce structured data there, or move the fields you need into that plugin’s own graph. Which of them removes it, and whether it does so by default, changes between their releases, so check your own page source rather than a list.

If you see two Product blocks in the page source, the second one is not from this plugin. Look at your SEO plugin or your theme.

The plugin logs nothing about structured data, so an empty debug.log says nothing either way. Read the page source, or the two tools above.

No added fields at all. Check that WooCommerce’s own structured data is on the page. If there is no Product block, the filter had nothing to run against.

A field is still missing after I filled it in. The product has to have the data and the matching switch has to be on, then the product has to be saved to clear the 12-hour cache.

No ratings. aggregateRating comes from WooCommerce, not from this plugin, and needs at least one review with a star rating.

Google shows no rich results. Rich results follow re-indexing, which takes weeks. Validate first, then wait.

Report issues: github.com/wppoland/polski/issues

This page is for informational purposes only and does not constitute legal advice. Consult a lawyer before implementation. Polski for WooCommerce is open source software (GPLv2) provided without warranty.