Sale price structured data: mark up Black Friday deals

Last updated:

To show a sale correctly in Product structured data, put the price shoppers pay now in offers.price, add the original price as a strikethrough price, and give the sale a start and end date. Search engines can then read the deal as a fact instead of guessing from your page design, which matters most during peak sales like Black Friday.

How sale price markup works

Google's merchant listing documentation describes the pattern:

  • The active price stays in offers.price and offers.priceCurrency. Don't give it a priceType.
  • The original price goes in offers.priceSpecification, as a UnitPriceSpecification with priceType set to https://schema.org/StrikethroughPrice (the only value supported there).
  • When that second price is present, the active price is treated as a sale price.
  • Dates: validFrom and validThrough give the start and end date-time of the price. You can use priceValidUntil instead of validThrough for the end, but use one or the other. All dates use ISO 8601, with a time zone.

Example

A shirt reduced from 79 € to 59 € for one week:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Linen shirt, sky blue",
  "image": "https://your-store.com/img/linen-shirt-front.jpg",
  "offers": {
    "@type": "Offer",
    "url": "https://your-store.com/products/linen-shirt-sky",
    "price": "59.00",
    "priceCurrency": "EUR",
    "availability": "https://schema.org/InStock",
    "validFrom": "2026-11-23T00:00:00+01:00",
    "priceValidUntil": "2026-11-30T23:59:59+01:00",
    "priceSpecification": {
      "@type": "UnitPriceSpecification",
      "priceType": "https://schema.org/StrikethroughPrice",
      "price": "79.00",
      "priceCurrency": "EUR"
    }
  }
}
</script>

Common mistakes

  1. Swapped prices. The original price ends up in offers.price and the sale price nowhere. The markup then contradicts the page.
  2. Markup out of sync with the page. The page shows the deal but the JSON-LD is cached from before the sale, or the reverse after it ends. Generate both from the same product data.
  3. No end date. Without validThrough or priceValidUntil, nothing in the markup says the deal is temporary.
  4. Both end properties at once, with different dates.
  5. A strikethrough price nobody ever paid. In the EU, a price reduction announcement must show the prior price, which is the lowest price applied in the 30 days before the reduction. Your crossed-out price should follow the same rule.

Keep your feed consistent

If you also send products to Google Merchant Center, the feed has its own sale attributes (sale_price and sale_price_effective_date). Make sure the feed, the page and the markup show the same prices for the same dates, so shoppers never land on a deal that no longer matches the ad or listing.

Sale markup builds on a complete Product structured data block. For the rest of your peak-season preparation, see the Black Friday checklist.

How Averifly helps

Averifly's catalog rules check that your product pages carry Product structured data with a price, currency and availability, and that a price is visible on the page. Run a free scan before your sale starts, and see our rules for the full list of checks.

Frequently asked questions

How do I mark a sale price in Product structured data?

Keep the current sale price in offers.price and add the original price as a second price in priceSpecification, with priceType set to https://schema.org/StrikethroughPrice. Google treats the active price as a sale price when that second price is present.

Should I use priceValidUntil or validThrough for the end of a sale?

Use one of them, not both. validFrom and validThrough give the start and end date-time of the price; priceValidUntil gives the date after which the price is no longer available. All use ISO 8601 format.

Do I need to remove the sale markup after the sale ends?

Yes. When the sale ends, the page and the markup should both return to the regular price. An end date helps, but your platform should still stop outputting the old sale price.

More Catalogue guides