I run the WooCommerce store behind a small online clothing shop. Most of the orders are made to order and ship from print partners in several countries, so the delivery promise differs by destination. Google's merchant listings can show that promise, and the return window, right in search results, but only if the product markup says so. WooCommerce's built-in Product schema does not.
This post walks through adding hasMerchantReturnPolicy and shippingDetails to the offer that WooCommerce already prints, without a plugin.
What WooCommerce outputs by default
WooCommerce prints JSON-LD for each product through the WC_Structured_Data class. The offer part comes from generate_product_data(), and before it is added to the markup it passes through a filter:
apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product );
That filter is the cleanest place to hook in. It receives one offer array and the WC_Product, and whatever you return becomes the offer in the final JSON-LD.
The return policy
Schema.org models returns as a MerchantReturnPolicy. For a fixed return window, Google wants the country it applies to, the category of the policy and the number of days. The shop in this example accepts returns for 30 days.
function shop_return_policy() {
return array(
'@type' => 'MerchantReturnPolicy',
'applicableCountry' => 'US',
'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
'merchantReturnDays' => 30,
'returnMethod' => 'https://schema.org/ReturnByMail',
);
}
Add returnFees only if you can state it truthfully for every product. Leaving out a property is better than guessing.
Shipping details per destination
Delivery time is split into handling time (the days before the parcel leaves) and transit time. Here processing takes 1 to 5 business days, and transit depends on the region, so the code builds one OfferShippingDetails entry per country:
function shop_shipping_details() {
$transit = array(
'US' => array( 8, 15 ),
'CA' => array( 8, 15 ),
'GB' => array( 8, 15 ),
'AU' => array( 10, 20 ),
);
$details = array();
foreach ( $transit as $country => $days ) {
$details[] = array(
'@type' => 'OfferShippingDetails',
'shippingDestination' => array(
'@type' => 'DefinedRegion',
'addressCountry' => $country,
),
'shippingRate' => array(
'@type' => 'MonetaryAmount',
'value' => SHOP_SHIPPING_RATE, // your real rate for this destination
'currency' => 'USD',
),
'deliveryTime' => array(
'@type' => 'ShippingDeliveryTime',
'handlingTime' => array(
'@type' => 'QuantitativeValue',
'minValue' => 1,
'maxValue' => 5,
'unitCode' => 'DAY',
),
'transitTime' => array(
'@type' => 'QuantitativeValue',
'minValue' => $days[0],
'maxValue' => $days[1],
'unitCode' => 'DAY',
),
),
);
}
return $details;
}
The rate is a constant here on purpose. If the rate differs by country or by cart total, read it from your shipping zones instead of hard-coding one number for everything.
Hooking it in
add_filter( 'woocommerce_structured_data_product_offer', function ( $offer, $product ) {
if ( ! $product instanceof WC_Product || ! $product->is_purchasable() ) {
return $offer;
}
$offer['hasMerchantReturnPolicy'] = shop_return_policy();
$offer['shippingDetails'] = shop_shipping_details();
return $offer;
}, 10, 2 );
Put this in a small site plugin or in the child theme's functions.php. A site plugin survives a theme switch, which is why I prefer it.
Checking the result
View the page source of a product and search for application/ld+json. WooCommerce prints the block near the end of the page. Then paste the product URL into Google's Rich Results Test and into the Schema Markup Validator at validator.schema.org. The first tells you whether the page is eligible for merchant listing features, and the second catches typos in types and property names.
After a few days, the Merchant listings report in Search Console shows whether Google has picked the fields up across the catalog, along with any products that are missing a required property.
Top comments (0)