Google’s product documentation separates two things that plenty of articles blur together: the properties required for a page to be eligible for a rich result, and the properties that enrich that result when they are present. Return and shipping information belongs to the second group. Search Central files them under result enhancements, alongside ratings, availability and price drops.
This article walks through Product markup as documented, section by section: what is mandatory, what is not, and where each block belongs on PrestaShop 8/9 and WooCommerce.
The mandatory base is five properties
On Product:
nameimage, one or more crawlable and indexable URLsoffers
On Offer:
price, orpriceSpecification.pricepriceCurrency, orpriceSpecification.priceCurrency
Three constraints come with it. Merchant listing experiences require an Offer rather than an AggregateOffer, since you are the seller. The price must be greater than zero. And the page must cover a single product or its variants, not a list or a category.
Google also separates two markup families. A product snippet targets pages where the visitor cannot buy directly, typically an editorial review, with extra options around review information. A merchant listing targets pages where the visitor buys from you, with apparel sizing, shipping and returns. Filling in the required merchant listing properties generally makes the page eligible for product snippets too.
Everything else is recommended
The recommended properties table for Offer holds availability, itemCondition, url, priceValidUntil, validFrom, validThrough, hasMerchantReturnPolicy and shippingDetails. On Product: brand.name, sku, mpn, the gtin family, description, category, color, size, material, pattern, audience, hasCertification, review, aggregateRating, inProductGroupWithID, isVariantOf and subjectOf.
These properties unlock display features: ratings, shipping cost and free shipping, availability, return information, price drops. Google states that enhancements are shown at the discretion of each experience and may change over time, and advises providing as much rich product information as you have rather than targeting one specific display.
The line that settles the argument: the release procedure asks you to fix critical errors flagged by the Rich Results Test, then adds that non-critical issues can improve the quality of your structured data but that fixing them is not necessary to be eligible for rich results. And “Missing field hasMerchantReturnPolicy” and “Missing field shippingDetails” are exactly what surfaces as non-critical in Search Console.
Returns and shipping belong at Organization level
For a return policy covering most or all of your catalogue, Google asks you to declare it once, on the page that describes that policy, in a MerchantReturnPolicy nested under Organization (or OnlineStore) through hasMerchantReturnPolicy. There is no need to repeat it on every page of the site.
At that level, two minimal markup options:
- Option A:
applicableCountryandreturnPolicyCategory. If the category isMerchantReturnFiniteReturnWindow, thenmerchantReturnDaysbecomes required. - Option B:
merchantReturnLink, the URL of the page describing the policy to customers.
The rest is recommended and lets you be precise: returnFees, returnMethod, returnShippingFeesAmount, returnPolicyCountry, refundType, restockingFee, returnLabelSource, itemCondition, the customerRemorse* and itemDefect* variants, plus returnPolicySeasonalOverride to tighten the window during holiday sales.
{
"@context": "https://schema.org",
"@type": "OnlineStore",
"name": "My store",
"url": "https://example.com",
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"@id": "https://example.com/returns#policy",
"applicableCountry": ["GB", "IE"],
"returnPolicyCountry": "GB",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn",
"refundType": "https://schema.org/FullRefund"
}
}
Offer level serves two cases only: overriding the standard policy for a specific product, or having no standard policy at all. The properties supported there are a subset of those available at store level. To reference the global policy from a product page without ambiguity, an @id is enough:
"hasMerchantReturnPolicy": { "@id": "https://example.com/returns#policy" }
Shipping follows the same pattern. The standard policy is declared under Organization, with a wider property set than the product level offers, and a product page can point to it through hasShippingService:
"shippingDetails": {
"@type": "OfferShippingDetails",
"hasShippingService": { "@id": "https://example.com/shipping#policy" }
}
If you do spell out shipping at Offer level, four properties are required for the enhancement: deliveryTime (with handlingTime and transitTime), shippingDestination (with addressCountry in ISO 3166-1 alpha-2), shippingRate.currency, and shippingRate.value or shippingRate.maxValue. One rate per OfferShippingDetails block: several rates means several blocks.
Markup sits last in the order of precedence
Google documents an order of precedence for return information, from strongest source to weakest:
- Content API for Shopping, return settings
- Settings in Merchant Center or Search Console
- Product-level merchant listing markup
Organization-level markup
Two practical consequences. If your returns are already configured in Search Console or Merchant Center, that configuration is what gets used and the markup becomes redundant. And a page without return markup does not read as “no return policy” to Google: it simply moves down the chain to find the information. For a catalogue where shipping rates change often, the documentation actually suggests Merchant Center over markup.
priceValidUntil is not a date to renew every year
The official definition: the date and time after which the price will no longer be available, in ISO 8601 format. The documentation adds one warning, and it is not the one repeated everywhere: your listing may not display if priceValidUntil indicates a past date. The risk comes from a stale date, not from a missing property.
Since the sale duration section was reworked, this property has a specific job: bounding a discount. The start goes in validFrom, the end in either validThrough or priceValidUntil. Google recommends providing both bounds, checking that the start is earlier than the end, and including the time and time zone.
"offers": {
"@type": "Offer",
"price": 10.00,
"priceCurrency": "GBP",
"validFrom": "2026-11-20T08:00:00+00:00",
"priceValidUntil": "2026-11-30T23:59:59+00:00",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"priceType": "https://schema.org/StrikethroughPrice",
"price": 15.00,
"priceCurrency": "GBP"
}
}
Watch the placement. On the Offer node, priceValidUntil and validThrough are interchangeable. On a PriceSpecification node, only validThrough works: priceValidUntil is not applicable there.
If your price has no real end date, leave priceValidUntil out. An auto-generated rolling one-year date describes nothing and amounts to announcing a sale deadline that does not exist.
Strikethrough price, member price, unit price
Three kinds of price are recognised, encoded in priceSpecification under Offer:
- Active price: neither
priceTypenorvalidForMemberTier. It can also stay at offer level inprice. - Strikethrough price:
priceTypeset tohttps://schema.org/StrikethroughPrice. This is what triggers sale display, the active price becoming the sale price. - Member price:
validForMemberTierpointing at aMemberProgramTierdefined in Merchant Center or in aMemberProgramunderOrganization.
The two markers do not combine: a price specification carrying both priceType and validForMemberTier is ignored. If you fill in both offers.price and offers.priceSpecification for the active price, Google uses offers.price.
For goods sold by volume, weight or length, unit pricing goes through referenceQuantity inside a UnitPriceSpecification. The documentation notes this format matters particularly in the EU, New Zealand and Australia.
Variants: ProductGroup, two possible structures
Only one property is required on ProductGroup: name. The group identifier is declared either with productGroupID on the ProductGroup (the parent SKU) or with inProductGroupWithID on each variant. If you provide both, they must match. variesBy lists the varying aspects, with six supported values: color, size, material, pattern, suggestedAge and suggestedGender.
Two structures are documented. Either the variants are nested inside the ProductGroup through hasVariant, or they are declared separately and point back to the parent with isVariantOf and an @id. Google recommends the first, describing it as the most compact and natural representation. The second is often easier to generate from a CMS.
The technical constraints matter as much as the markup. Each variant needs a unique identifier (sku or gtin) and a distinct URL that preselects it, with the right image, price and availability, and the ability to add it to the cart. On a single-page site, one canonical URL represents the group. On a multi-page site, each page needs full self-contained markup, with the ProductGroup definition repeated.
Energy labels: hasCertification
For appliances, bulbs or screens sold in the EU, the property to use is hasCertification, with a Certification object:
issuedBy.name:ECorEuropean_Commissionfor EU energy labels,ADEMEandBMWKfor vehicle CO2 classes.name:EPREL,Vehicle_CO2_ClassorVehicle_CO2_Class_Discharged_Battery.certificationIdentification: the EPREL code, required for European energy labels.certificationRating: for cases where no EPREL code exists (Norway, Switzerland, UK) or for CO2 classes.ratingValueis required, and for energy efficiency so arebestRatingandworstRating.
Up to ten certifications per product. The older hasEnergyConsumptionDetails property is still read, but the documentation recommends moving to hasCertification.
What about AI Overviews?
No public Google documentation makes appearance in AI Overviews conditional on hasMerchantReturnPolicy, shippingDetails or any other Product property. These properties are documented for merchant listing experiences: the shopping knowledge panel, popular products, Google Images, Google Lens and product snippets.
Clean markup helps Google’s systems understand the page, which is reason enough to get it right. But until documentation or a reproducible study establishes it, there is no basis for allocating budget on a causal link between these properties and generative answers.
Implementation on PrestaShop 8 and 9
The Classic theme outputs Product JSON-LD with offers, but neither the return and shipping policies nor the ProductGroup structure for variants. Two separate jobs, worth keeping apart.
Catalogue side. Either override the template that emits the JSON-LD in your child theme, with the maintenance that implies at every version bump, or use a module. Either way, check that a single Product block is emitted per page: two active SEO modules produce two competing sets of markup.
Store side. The return policy and the shipping policy are declared once, on the matching CMS pages, in an Organization block. The simplest route is a custom code field injected into the head of those two pages.
The DataFirefly All in One SEO module covers the first job: a JSON-LD graph with Organization, WebSite, BreadcrumbList, Product including offers, priceValidUntil and the AggregateRating pulled from productcomments, plus FAQPage and LocalBusiness. It also ships head and body custom code fields, where the store-level policies belong.
Implementation on WooCommerce
WooCommerce natively emits a Product with offers, editable through the woocommerce_structured_data_product filter. Yoast SEO and Rank Math each build their own graph and expose their own extension point. Pick one of these three sources: stacking them means publishing several Product blocks on the same URL.
For policies, the logic matches PrestaShop: an Organization block on the returns page and on the shipping page, referenced from product pages by @id if you want to remove any ambiguity. The llms.txt + AEO Schema WooCommerce plugin handles that graph and the agent-facing FAQ.
Checking the result
- Rich Results Test. Fix the critical errors. Non-critical warnings are your call: decide based on the enhancements you want.
- Schema.org validator. JSON-LD syntax and
@typeconsistency, independently of Google rules. - Search Console, Merchant listings report. The real production state, once pages have been recrawled. Allow several days after publishing.
- Merchant Center or Search Console return settings. Check that an existing configuration is not already overriding your markup.
Where to start
- Confirm that
name,image,offers,priceandpriceCurrencyare present and accurate on 100% of product pages. This is the only genuine blocker. - Add
availability,itemCondition,sku,brand.name, and the GTIN where one exists. - Declare the return policy and the shipping policy once, at
Organizationlevel, on their dedicated pages. - Only set
priceValidUntilon prices with a real end date, paired withvalidFrom. - Handle variants with
ProductGroup, once you have confirmed every variant has its own URL.
One rule sits above all of this: markup must describe what the page shows. A free return in the markup must be advertised on the site, a 30-day window in the markup must be a real 30-day window. That principle, not the number of properties, is behind most manual actions on structured data.
Also worth reading: the complete e-commerce SEO guide and FAQ schema and rich snippets.
To put this into practice: our selection of modules to optimise your product pages.