Google has recently created a stir with their recent structured data warnings about missing shipping information in the Merchant Listings report (“Missing field ‘shippingDetails’ (in ‘offers’)”) backed up with an email to worry everyone (“Missing shipping information in your Merchant Listings on your sites”). Currently, I suspect most CMSes do not provide a way to get hold of and create this shipping information. So I built a JavaScript template to help you make your own advanced shipping information.

Do you need it?

The presence of shipping information on a product page means you can be eligible for the shipping based rich snippets. This can help consumers get a better idea on how much your products will eventually cost, without them having to visit your site and fill in details. Giving you an edge over your competition, especially if you offer free shipping.

If you have a Merchant Centre account and have filled in your shipping information there (via feed, via settings), you do not need to also add it to your website. But just adding one shipping item will remove the warning and may increase your chances of getting the snippets.

Complex pricing with shipping providers

Some sites use APIs with shipping providers to get highly variable shipping prices based on factors like destination, size and weight. It would be practically impossible to mark up all the options, and if you did, your page would be massive. So how do you deal with this?

I suggest you mark up what you can and what will benefit you the most. Try and indicate when you offer free shipping, as that benefits you the most. Do you offer free shipping for certain destinations or product price levels?

Then you could mark up maximum costs for different destinations, product sizes, and weights.

In the future, Google may let you define well-known shipping providers so that they can work out the shipping for you. This is already possible in the Merchant Centre if the required product attributes (weight and dimensions) are included.

Our BigCommerce Shipping Information solution

Our SEO Rich Snippets app for BigCommerce includes the ability to define basic shipping information for your products. If you need a more complex solution, I have created a script for BigCommerce that gathers product information and includes some example scenarios.

Connecting Shipping Information to your Product Offer markup

It’s important that all your structured data is correctly linked together. This solution assumes you already have Product Structured Data on your product pages. It’s not going to try and replace it. It is going to link shipping details with it.

Shipping details structured data is part of the product’s Offer. So we need to merge it into your existing Offer on the page. This is done by indicating that our shipping details is part of the same offer. To do that, it needs to use the same “@id”. In the following example, the shippingDetails will be added to the products Offer because the two separate items use the same “@id”.

<!-- Existing Product schema -->
<script type="application/ld+json">
{
    "@context":"https://schema.org",
    "@type":"Product",
    "offers": [{
        "@type": "Offer",
        "@id": "#offer"
        "price": 100,
        "priceCurrency": "USD",
        ...
    }],
    ...
}
</script>

<!-- Shipping Information schema -->
<script type="application/ld+json">
{
    "@context":"https://schema.org",
    "@id": "#offer"
    "shippingDetails": [...],
    ...
}
</script>

So the first thing in our script is a variable to set the value for the “@id”. The script will then merge its shipping information correctly.

var offerId = '#offer';

I’ve also provided a complete script template to help you get started with your solution…

The Shipping Information items

The next part of the code is about adding your list of shipping information items to add to the product. These items are stored in an array:

shippingItems = [];

Each item can have the following parameters.

ParameterDescription
costThe cost of shipping to the specified destination. One of cost or maxCost is required. A number.
maxCostThe maximum cost of shipping to the specified destination. One of cost or maxCost is required. A number.
currencyRequired. The currency code of the cost. e.g. “USD”. The currency should be the same as submitted for the offer price.
countryRequired. 2 letter country codes. You can specify more than one in an array. e.g. [“US”, “CA”]
regionOptional. Do not combine with postalCode. 2 or 3 letter codes. Google currently supports it in US, Australia, and Japan. You can specify more than one in an array. e.g. [“NY”, “CA”]
postalCodeOptional. Do not combine with region. Google currently support it in Australia, Canada, and the US. You can specify more than one in an array, and you may also be able to supply ranges and wild cards. e.g. Metropolitan Adelaide: [“5000-5117”, “512*”, “5150”, “5158”, “5159”, “516*-517*”, “5950”, “5960”].
handlingTimeMinDaysRequired. The typical minimum delay in days between the receipt of the order and the goods leaving the warehouse.
handlingTimeMaxDaysRequired. The typical maximum delay in days between the receipt of the order and the goods leaving the warehouse.
transitTimeMinDaysRequired. The typical minimum delay in days between when the order has been sent for delivery and when the goods reach the final customer.
transitTimeMaxDaysRequired. The typical maximum delay in days between when the order has been sent for delivery and when the goods reach the final customer.

Some examples

This example items js file contains all the examples that follow.

All shipping in a country is free

If you can do this, you will look better than your competitors. The main thing here is to specify where you offer free shipping. You don’t want to have to pay to ship to Antarctica. And remember to fill in the correct delivery times.

shippingItems.push({
    cost: 0, // FREE
    currency: "USD",
    country: ["US"], // In the US
    handlingTimeMinDays: 0, 
    handlingTimeMaxDays: 1,
    transitTimeMinDays: 1,
    transitTimeMaxDays: 3
});

Shipping in some postcodes are free

Let’s make it free around where I live (the Adelaide Metropolitan postcodes).

shippingItems.push({
    cost: 0, // FREE
    currency: "AUD",
    country: ["AU"], // In Australia
    postalCode: ["5000-5117", "512*", "5150", "5158", "5159", "516*-517*", "5950", "5960"], // Adelaide Metropolitan postcodes
    handlingTimeMinDays: 0, 
    handlingTimeMaxDays: 1,
    transitTimeMinDays: 1,
    transitTimeMaxDays: 3
});

Shipping is a fixed price for the rest of my state

Let’s say, max transit time for the state can be a bit longer

shippingItems.push({
    cost: 10, 
    currency: "AUD",
    country: ["AU"], 
    region: ["SA"],// In South Australia
    handlingTimeMinDays: 0, 
    handlingTimeMaxDays: 1,
    transitTimeMinDays: 1,
    transitTimeMaxDays: 5
});

Shipping will never exceed a price

Here we use the maxCost instead of cost. So say our shipping provider will never charge more than $15 in Australia.

shippingItems.push({
    maxCost: 15, 
    currency: "AUD",
    country: ["AU"], 
    handlingTimeMinDays: 0, 
    handlingTimeMaxDays: 1,
    transitTimeMinDays: 3,
    transitTimeMaxDays: 7
});

Free shipping for products over a certain price

Here we need to get hold of some product information. This is how you can do it in BigCommerce.

var bigCommerceDecimalToken = '.';
var bigCommerceThousandsToken = ','

function parseBigCommerceDimension(text) {
    var newText = text.toLowerCase().replace("{{settings.measurements.length}}".toLowerCase(), "").replace("{{settings.measurements.weight}}".toLowerCase(), "").replace(bigCommerceDecimalToken, ".").replace(bigCommerceThousandsToken, "").trim();

    if (newText) {
        return parseFloat(newText);
    }
    else return -1;
}

var product = {
        price: {{~#if product.price.with_tax}}{{product.price.with_tax.value}}{{else}}{{~#if product.price.without_tax}}{{product.price.without_tax.value}}{{else}}-1{{~/if}}{{~/if}}, // -1 = price unknown
        priceCurrency: '{{~#if product.price.with_tax}}{{product.price.with_tax.currency}}{{else}}{{~#if product.price.without_tax}}{{product.price.without_tax.currency}}{{else}}{{~/if}}{{~/if}}', 
        weight: parseBigCommerceDimension('{{product.weight}}'), 
        width: parseBigCommerceDimension('{{product.width}}'),
        height: parseBigCommerceDimension('{{product.height}}'), 
        depth: parseBigCommerceDimension('{{product.depth}}'),
        fixedShippingPrice: {{~#if product.shipping.price}}{{product.shipping.price.value}}{{else}}-1{{~/if}}, // -1 = no fixed price. 0 = free
        fixedShippingCurrency: '{{~#if product.shipping.price}}{{product.shipping.price.currency}}{{~/if}}'
    };

With that product information, we can now do some fun things. Let’s do free shipping in Australia for products over $100.

if (product.price > 100) {
    shippingItems.push({
        cost: 0, 
        currency: "GBP",
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
} else {
  // Costs for products under $100
}

Please share any solutions you have to get product information on other platforms.

Using a fixed shipping price

In BigCommerce, you can specify a product’s exact shipping price, including free shipping. So it makes sense for that to override other options.

if (product.fixedShippingPrice >= 0) {
    shippingItems.push({
        cost: product.fixedShippingPrice, 
        currency: product.fixedShippingCurrency,
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
} else {
    // non fixed price shipping options
}

Using formulas for shipping cost

You may vary your shipping cost by other factors like weight. Maybe different costs for different weight ranges. Or even a special formula.

if (product.weight < 10) {
    shippingItems.push({
        cost: 5, // min shipping cost
        currency: "GBP",
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
} else if (product.weight < 20) {
    shippingItems.push({
        cost: 10, 
        currency: "GBP",
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
} else if (product.weight > 100) {
    shippingItems.push({
        cost: 50, // the max shipping cost
        currency: "GBP",
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
} else {
    shippingItems.push({
        cost: 10 + (product.weight / 3), // varying shipping cost
        currency: "GBP",
        country: ["GB"], 
        handlingTimeMinDays: 0, 
        handlingTimeMaxDays: 1,
        transitTimeMinDays: 3,
        transitTimeMaxDays: 7
    });
}

The code to build the structured data

Once you have populated your shippingItems array, add the following code, which will build and add the required structured data to your page:

var shippingDetails = [];

    for (var i = 0; i < shippingItems.length; i++) {
        var item = shippingItems[i];

        var shippingRate = {"@type": "MonetaryAmount"};
        if (item.maxCost !== 'undefined') shippingRate.maxValue = item.maxCost;
        if (item.cost !== 'undefined') shippingRate.value = item.cost;
        if (item.currency) shippingRate.currency = item.currency;

        var shippingDestination = {"@type": "DefinedRegion"};
        if (item.country) shippingDestination.addressCountry = item.country;
        if (item.region) shippingDestination.addressRegion = item.region;
        if (item.postalCode) shippingDestination.postalCode = item.postalCode;

        var handlingTime = {"@type": "QuantitativeValue", "unitCode": "DAY"};
        if (item.handlingTimeMinDays !== 'undefined') handlingTime.minValue = item.handlingTimeMinDays;
        if (item.handlingTimeMaxDays !== 'undefined') handlingTime.maxValue = item.handlingTimeMaxDays;

        var transitTime = {"@type": "QuantitativeValue", "unitCode": "DAY"};
        if (item.transitTimeMinDays !== 'undefined') transitTime.minValue = item.transitTimeMinDays;
        if (item.transitTimeMaxDays !== 'undefined') transitTime.maxValue = item.transitTimeMaxDays;

        shippingDetails.push({
            "@type": "OfferShippingDetails",
            "shippingRate": shippingRate,
            "shippingDestination": shippingDestination,
            "deliveryTime": {
                "@type": "ShippingDeliveryTime",
                "handlingTime": handlingTime,
                "transitTime": transitTime
            }
        });
    }

    var offer = {
        "@context":"https://schema.org",
        "@id": offerId,
        "shippingDetails": shippingDetails
    };

    var script = document.createElement('script');
    script.id = 'wsa-rich-snippets-jsonld-offer-shipping';
    script.setAttribute('type', 'application/ld+json');
    script.textContent = JSON.stringify(offer);
    document.head.appendChild(script);

Summary

The complete template script is a great starting point to help you include your shipping info on your product pages. Remember that just adding a few shipping options, especially the free ones, can make you stand out from your competitors.

References