# Product schema: price, availability and reviews for rich results

> Product markup puts price, stock and stars under your listing. Learn what Google requires, how to write offers, variants and ratings, and why shops end up with two Products.

Updated 2026-09-25 · Structured data · HTML version: https://getreport.app/guides/product-schema-price-availability-reviews

A product listing in Google can be a plain blue link, or it can show "€129.00 · In stock · ★ 4.6 (84)" under the title. The difference is a block of Product structured data with the right properties filled in the right format. Most shops already have some Product markup from their platform; what they lack is one property, one format detail or one duplicate removed. This guide shows what Google requires, how each field should look, and how to check a product page in a minute.

## Quick answer

- A product page needs one Product entity with `name`, `image` and `offers`.
- The Offer needs `price` (a number with a dot, no currency sign), `priceCurrency` (three-letter code such as `EUR`) and should have `availability` as a schema.org URL (`https://schema.org/InStock`).
- Ratings go in `aggregateRating` with `ratingValue` and `reviewCount` (or `ratingCount`), and only when the reviews are shown on the page.
- Sizes and colours are variants: one Offer per variant, or a ProductGroup with `hasVariant`.
- One Product per product. WooCommerce plus an SEO plugin, or Shopify plus a review app, can print two.
- Run the [schema validator](https://getreport.app/tools/schema-validator) on a product page and fix what it lists.

## Why product schema matters

Search results with a price, stock status and stars are larger, answer the shopper's first two questions before the click, and stand out against plain listings on the same page. Product markup is also what makes a page eligible for Google's shopping surfaces beyond the classic result: the Shopping tab, product grids and image results with a price badge.

Google recognises two kinds of product page. **Product snippets** are for pages about a product, including review sites that do not sell it; they need a `name` and at least one of `review`, `aggregateRating` or `offers`. **Merchant listings** are for pages where the visitor can buy the product; they need `name`, `image` and an Offer with a price and currency, and they are what a shop should aim for. Google's [product snippet documentation](https://developers.google.com/search/docs/appearance/structured-data/product) and [merchant listing documentation](https://developers.google.com/search/docs/appearance/structured-data/merchant-listing) list every property.

Google only shows product rich results for pages that focus on a single product, or on variants of one product. Category pages listing twenty products do not qualify, however they are marked up.

## How getReport checks it

> **Free tool:** [Schema markup validator](https://getreport.app/tools/schema-validator): Find and validate JSON-LD and microdata on any page: parse errors, missing required properties, rich-result eligibility and duplicate entities, with the fix for each.

The validator reads the JSON-LD in the product page's HTML, lists every top-level entity, and checks each Product for the merchant-listing basics: `name`, `image`, `offers`, a price (`offers.price`, or `offers.lowPrice` for a price range) and `offers.priceCurrency`. When `offers` is missing altogether, only `offers` is reported; add it, and the finding moves on to what the Offer lacks.

> **Check: Required schema properties.** Google shows a rich result (price, stars, breadcrumb, event date) only when the entity carries the properties it requires. Without them the block is valid but earns nothing in search.
>
> 1. Open the technical detail for the full list, one line per entity type and property.
> 2. Add each property to the JSON-LD block. In WooCommerce, Yoast or Rank Math this usually means filling in the price, image or author field on the item itself.

![The required properties finding on a product page whose Product block has no offers: the title names the missing property and the Product type, and the detail lists each gap as type and property](https://getreport.app/guides/img/product-schema-price-availability-reviews/finding.webp "Each line in the detail is one property to add, in the form type: property.")

> **Check: Rich result eligibility.** Rich results (price, stars, breadcrumbs or event dates under your listing) make it bigger and earn more clicks than a plain blue link. An entity with missing required properties does not qualify.
>
> 1. Fix the properties listed under "required properties missing" first; eligibility follows automatically.
> 2. Test the page with Google's Rich Results Test after the change.

> **Check: Structured data types.** These are the entities the page declares. Google can only show rich results for the types it documents, so the list tells you what you can earn from this page.
>
> 1. Compare the list with the rich-result types Google supports (Product, Article, Event, BreadcrumbList, LocalBusiness…) and add the one that fits this page.

On a product page, you want to see `Product` once, next to the site-wide entities (`Organization`, `WebSite`, `BreadcrumbList`). `Product ×2` means two blocks describe the same product.

> **Check: Structured data entities are unique.** Two blocks describing the same Organization, WebSite or Product make Google pick one at random, and their properties may disagree. Usually a theme and an SEO plugin both emit the block.
>
> 1. Keep one block per entity; turn off the schema output of either the theme or the SEO plugin.
> 2. If both are needed, merge them into one @graph with a single entity per @id.

Two limits worth knowing. getReport checks entities that sit at the top level of a block or in an `@graph`. An `aggregateRating` nested inside the Product, and the variants inside a `ProductGroup`, are not checked property by property; the Rich Results Test covers those. And a review site without prices will still be asked for `offers`, because the check follows the merchant-listing requirements; if you do not sell the product, that line can be ignored.

## Step by step

### 1. Start from what the platform prints

Run the validator on one product page. If the types finding already shows `Product`, you are editing, not writing: find where the block comes from (the theme, WooCommerce, an SEO or review plugin) and fix the data or the settings there. Hand-adding a second block is how duplicates happen.

### 2. Write the Offer correctly

The Offer is where most errors live, because each field has a format:

| Property | Correct | Wrong |
| --- | --- | --- |
| `price` | `129.00` or `"129.00"` | `"€129"`, `"129,00"`, `"1.299,00"` |
| `priceCurrency` | `"EUR"`, `"USD"`, `"GBP"` (ISO 4217) | `"€"`, `"euro"` |
| `availability` | `"https://schema.org/InStock"` | `"In stock"`, `"available"` |
| `itemCondition` | `"https://schema.org/NewCondition"` | `"new"` |
| `priceValidUntil` | `"2026-12-31"` (a future date) | a date that has passed |
| `url` | the product (or variant) page URL | the home page |

The availability values Google reads are `InStock`, `OutOfStock`, `PreOrder`, `BackOrder`, `LimitedAvailability`, `OnlineOnly`, `InStoreOnly`, `SoldOut` and `Discontinued`, each as a full `https://schema.org/…` URL. `priceValidUntil` is optional; set it only if the price really ends on a date, because a date in the past can stop the snippet from showing.

The price must be the one a visitor sees and pays on the page, in the same currency. If the shop shows prices in several currencies, the markup follows the one on the page as served.

### 3. Add shipping and returns if you sell online

Google uses `shippingDetails` and `hasMerchantReturnPolicy` to show delivery costs and return windows in shopping results. They are recommended, not required. A store-wide return policy can also be declared once on your Organization markup or set in Google Merchant Center instead of on every product. The complete example below includes both.

### 4. Add ratings only from reviews on the page

An `aggregateRating` needs `ratingValue` and a count: `reviewCount` (reviews with text) or `ratingCount` (star ratings). Without a count, Google does not show stars. If your scale is not 1 to 5, add `bestRating` and `worstRating`.

Google's [review snippet guidelines](https://developers.google.com/search/docs/appearance/structured-data/review-snippet) come down to three rules for shops:

- The reviews must be visible on the product page, not only in the markup.
- They must come from real customers, collected by you or by your review app, not copied from other websites.
- The rating must be about this product. Stars in your Organization or LocalBusiness markup from reviews on your own site are "self-serving" and are not shown; this restriction does not apply to customer reviews of a product.

### 5. Handle variants

If a shoe comes in six sizes with one price and one page, a single Offer is fine, with `availability` for the product as a whole. When variants differ in price or stock, give each its own entry. Google documents `ProductGroup` for this: one group with `productGroupID`, `variesBy` and a `hasVariant` list of Products, each with its own `sku`, variant property and Offer (see Google's [product variants documentation](https://developers.google.com/search/docs/appearance/structured-data/product-variants)).

```json
{
  "@context": "https://schema.org",
  "@type": "ProductGroup",
  "@id": "https://example.com/shoes/alpina-pro/#productgroup",
  "name": "Alpina Pro trekking shoe",
  "productGroupID": "ALP-PRO",
  "brand": { "@type": "Brand", "name": "Alpina" },
  "variesBy": ["https://schema.org/size"],
  "hasVariant": [
    {
      "@type": "Product",
      "sku": "ALP-PRO-41",
      "name": "Alpina Pro trekking shoe, size 41",
      "size": "41",
      "image": "https://example.com/img/alpina-pro-1.webp",
      "offers": {
        "@type": "Offer",
        "url": "https://example.com/shoes/alpina-pro/?size=41",
        "price": 129.00,
        "priceCurrency": "EUR",
        "availability": "https://schema.org/InStock"
      }
    },
    {
      "@type": "Product",
      "sku": "ALP-PRO-42",
      "name": "Alpina Pro trekking shoe, size 42",
      "size": "42",
      "image": "https://example.com/img/alpina-pro-1.webp",
      "offers": {
        "@type": "Offer",
        "url": "https://example.com/shoes/alpina-pro/?size=42",
        "price": 129.00,
        "priceCurrency": "EUR",
        "availability": "https://schema.org/OutOfStock"
      }
    }
  ]
}
```

A simpler alternative for a price range is an `AggregateOffer` with `lowPrice`, `highPrice`, `priceCurrency` and `offerCount`; it works for product snippets, but it cannot say which size is out of stock.

### 6. Put it together

A complete Product for a simple product page, in a `<script type="application/ld+json">` element in the product template. It passes every getReport check and carries the recommended merchant-listing fields:

```json
{
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": "https://example.com/shoes/alpina-pro/#product",
  "name": "Alpina Pro trekking shoe",
  "description": "Waterproof trekking shoe with a grippy rubber sole, 780 g per pair.",
  "image": [
    "https://example.com/img/alpina-pro-1.webp",
    "https://example.com/img/alpina-pro-2.webp"
  ],
  "sku": "ALP-PRO",
  "brand": { "@type": "Brand", "name": "Alpina" },
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/shoes/alpina-pro/",
    "price": 129.00,
    "priceCurrency": "EUR",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition",
    "shippingDetails": {
      "@type": "OfferShippingDetails",
      "shippingRate": { "@type": "MonetaryAmount", "value": 4.90, "currency": "EUR" },
      "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "HR" },
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
        "transitTime": { "@type": "QuantitativeValue", "minValue": 2, "maxValue": 4, "unitCode": "DAY" }
      }
    },
    "hasMerchantReturnPolicy": {
      "@type": "MerchantReturnPolicy",
      "applicableCountry": "HR",
      "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
      "merchantReturnDays": 30,
      "returnMethod": "https://schema.org/ReturnByMail",
      "returnFees": "https://schema.org/FreeReturn"
    }
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.6,
    "reviewCount": 84
  },
  "review": [
    {
      "@type": "Review",
      "author": { "@type": "Person", "name": "Marko K." },
      "datePublished": "2026-08-14",
      "reviewRating": { "@type": "Rating", "ratingValue": 5 },
      "reviewBody": "Dry feet after a full day of rain on Velebit."
    }
  ]
}
```

Every value here must appear on the page: the price, the stock status, the rating and the review. For the general rules of JSON-LD (where the script goes, `@id`, escaping), see [JSON-LD basics](https://getreport.app/guides/json-ld-basics).

### 7. Re-run and confirm with Google

Run the validator again: required properties should pass, the eligibility finding should list Product as eligible, and the types should show one Product. Then run Google's Rich Results Test on the same URL; it also checks the nested rating, the variants and the shipping and return fields.

## Platform notes

### WordPress and WooCommerce

WooCommerce prints Product JSON-LD on every product page by itself, built from the product's price, stock status, images and reviews. Variable products get an `AggregateOffer` with the lowest and highest variant price. Most gaps are data gaps: a product without a featured image has no `image`, a product with an empty price has no `price`. Fix them on the product itself.

The doubling happens when an SEO plugin adds its own Product. Yoast's WooCommerce add-on and Rank Math both integrate with WooCommerce and are meant to produce one Product inside their graph; when that integration is off, or a third schema plugin joins, the page carries two. The types finding then shows `Product ×2`; when both blocks carry the same name and URL, the duplicate check lists the product by name. If two SEO or schema plugins each print a graph, the WordPress checks name both. Keep one source, as described in [schema markup validation](https://getreport.app/guides/schema-markup-validation).

### Shopify

Shopify themes print Product JSON-LD from the product template; newer themes use Shopify's `structured_data` Liquid filter to build it from the product object. Review apps add ratings in one of two ways: by extending the theme's Product, or by printing their own Product block with an `aggregateRating`. The second way creates a duplicate. Check the app's settings for a "rich snippets" or "schema" option, and keep one Product carrying both the offer and the rating.

### Static sites and custom builds

Generate the block from the same product record that renders the page, so price and stock can never disagree with what the visitor sees. Cached pages are a special risk: when a product sells out, the cached HTML may keep saying `InStock` until the cache expires.

## Verify

- The validator shows one Product, no missing required properties, and Product among the types eligible for rich results. The [rich results learn page](https://getreport.app/learn/rich-results) shows what each result looks like.
- Google's Rich Results Test lists "Merchant listings" and "Product snippets" as valid, with no errors on the rating or the Offer.
- After the next crawl, Search Console's shopping and product reports show the page as valid. Stars and prices can take a few weeks to appear, and Google decides per search whether to show them.

## Common mistakes

- **Price with a currency sign or comma decimal.** `"€129,00"` is not a number. Use `129.00` and put the currency in `priceCurrency`.
- **Availability as plain text.** `"In stock"` is not recognised. Use `https://schema.org/InStock`.
- **Rating without a count.** `aggregateRating` with only `ratingValue` earns no stars. Add `reviewCount` or `ratingCount`.
- **Markup for products not on the page.** A category page marked up with twenty Products, or a rating that appears only in the markup, is against Google's guidelines. One product (or product group) per page, and every value visible.
- **Two Products from two sources.** Theme plus plugin, or plugin plus review app. The types list shows `Product ×2`; turn one off rather than trying to make them agree.
