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.
What comes from where
Section titled “What comes from where”| 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.
Switching it on and off
Section titled “Switching it on and off”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.
The fields Polski adds
Section titled “The fields Polski adds”Brand and manufacturer
Section titled “Brand and manufacturer”{ "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.
Unit price
Section titled “Unit price”Schema.org has no unit-price property, so the figure rides in additionalProperty:
{ "additionalProperty": [ { "@type": "PropertyValue", "name": "Unit price", "value": "8.90 / 100 g" } ]}Delivery time
Section titled “Delivery time”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.
Nutrition
Section titled “Nutrition”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.
priceValidUntil and the Omnibus price
Section titled “priceValidUntil and the Omnibus price”{ "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.
Caching
Section titled “Caching”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.
Extending
Section titled “Extending”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.
SEO plugins
Section titled “SEO plugins”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.
Validating
Section titled “Validating”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.
Troubleshooting
Section titled “Troubleshooting”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