bigcommerce / cornerstone

The BigCommerce Cornerstone theme
https://developer.bigcommerce.com/stencil-docs
286 stars 609 forks source link

Rich results returning errors for products with price range #2153

Open sacr3dc0w opened 2 years ago

sacr3dc0w commented 2 years ago

Expected behavior

Structured data to be parsed by Google without error.

Actual behavior

If conditional of {{#if product.price.price_range}} is met, an error gets thrown, as there's no price for the Offer.

Steps to reproduce behavior

Always displaying price does the trick for me.

Link to rich results (I'll leave up for 24 hours, but will be pushing the fix below definitely before Wednesday): https://search.google.com/test/rich-results/result?id=f09f3CeP4GFgAlRExDpa9w

templates/components/products/schema.html

BEFORE:

        "offers": {
            "@type": "Offer",
            "priceCurrency": "{{currency_selector.active_currency_code}}",
            {{#if product.price.price_range}}
            "minPrice": "{{#if product.price.price_range.min.with_tax}}{{product.price.price_range.min.with_tax.value}}{{else}}{{product.price.price_range.min.without_tax.value}}{{/if}}",
            "maxPrice": "{{#if product.price.price_range.max.with_tax}}{{product.price.price_range.max.with_tax.value}}{{else}}{{product.price.price_range.max.without_tax.value}}{{/if}}",
            {{else}}
            "price": "{{#if product.price.with_tax}}{{product.price.with_tax.value}}{{else}}{{product.price.without_tax.value}}{{/if}}",
            {{/if}}
            "itemCondition" : "https://schema.org/{{#if product.condition}}{{product.condition}}{{else}}New{{/if}}Condition",
            "availability" : "https://schema.org/{{#if product.pre_order}}PreOrder{{else if product.out_of_stock}}OutOfStock{{else if product.can_purchase '===' false}}OutOfStock{{else}}InStock{{/if}}",
            "url" : "{{product.url}}",
            "priceValidUntil": "{{moment (moment add='31536000000') 'YYYY-MM-DD' }}"
        }

AFTER:

        "offers": {
            "@type": "Offer",
            "priceCurrency": "{{currency_selector.active_currency_code}}",
            {{#if product.price.price_range}}
            "minPrice": "{{#if product.price.price_range.min.with_tax}}{{product.price.price_range.min.with_tax.value}}{{else}}{{product.price.price_range.min.without_tax.value}}{{/if}}",
            "maxPrice": "{{#if product.price.price_range.max.with_tax}}{{product.price.price_range.max.with_tax.value}}{{else}}{{product.price.price_range.max.without_tax.value}}{{/if}}",
            {{/if}}
            "price": "{{#if product.price.with_tax}}{{product.price.with_tax.value}}{{else}}{{product.price.without_tax.value}}{{/if}}",
            "itemCondition" : "https://schema.org/{{#if product.condition}}{{product.condition}}{{else}}New{{/if}}Condition",
            "availability" : "https://schema.org/{{#if product.pre_order}}PreOrder{{else if product.out_of_stock}}OutOfStock{{else if product.can_purchase '===' false}}OutOfStock{{else}}InStock{{/if}}",
            "url" : "{{product.url}}",
            "priceValidUntil": "{{moment (moment add='31536000000') 'YYYY-MM-DD' }}"
        }
Tiggerito commented 2 years ago

That's how we do it.

Tiggerito commented 2 years ago

An update here:

minPrice and maxPrice are not part of an Offer, but they can be added to a PriceSpecification.

Google does support AggregateOffer, which has a lowPrice and highPrice, but it is not appropriate for BigCommerce stores. It is for product aggregators. Its use may exclude a product from merchant listings. So I would not use AggregateOffer.

https://developers.google.com/search/docs/appearance/structured-data/product#aggregate-offer-properties

RobDNZ commented 1 year ago

Hello,

We ran into the same problem and have implemented the same fix including adding the PriceSpecification element as follows:

"offers": {
            "@type": "Offer",
            "priceCurrency": "{{currency_selector.active_currency_code}}",
            {{#if product.price.price_range}}
            "priceSpecification": {
                "minPrice": "{{#if product.price.price_range.min.with_tax}}{{product.price.price_range.min.with_tax.value}}{{else}}{{product.price.price_range.min.without_tax.value}}{{/if}}",
                "maxPrice": "{{#if product.price.price_range.max.with_tax}}{{product.price.price_range.max.with_tax.value}}{{else}}{{product.price.price_range.max.without_tax.value}}{{/if}}"
            },
            {{/if}}
            "price": "{{#if product.price.with_tax}}{{product.price.with_tax.value}}{{else}}{{product.price.without_tax.value}}{{/if}}",
            "itemCondition" : "https://schema.org/{{#if product.condition}}{{product.condition}}{{else}}New{{/if}}Condition",
            "availability" : "https://schema.org/{{#if product.pre_order}}PreOrder{{else if product.out_of_stock}}OutOfStock{{else if product.can_purchase '===' false}}OutOfStock{{else}}InStock{{/if}}",
            "url" : "{{product.url}}",
            "priceValidUntil": "{{moment (moment add='31536000000') 'YYYY-MM-DD' }}"
        }

After this the code validates correcly both in the schema.org validator and google rich results test.

https://developers.google.com/search/docs/appearance/structured-data

Tiggerito commented 1 year ago

I'd also test that it causes the desired rich snippets in Google, as that is what helps your clients.

RobDNZ commented 1 year ago

I don't think we can actually test this before it is pushed to the live store but we will definitely test this first and monitor this after deployment to production.

RobDNZ commented 1 year ago

The changes have been deployed to our live store and our marketing department has confirmed that they are seeing the expected improvements. In our case the improvements are even bigger because this store did not have the JSON-LD structure yet (they still had the old meta tag stuff).