You built a beautiful product page. Fast loading, clean design, great copy. Then you check Google Search Console and see your competitor's listing showing star ratings, price, and availability — right in the search results. Yours shows just a title and a meta description.
That gap is Product Schema Markup.
This guide covers everything: what it is, how Google uses it, which fields actually matter, common mistakes that silently break your rich results, and implementation examples for vanilla HTML, Next.js, and React.
What Is Product Schema Markup?
Product Schema is a vocabulary from schema.org/Product that you embed in your HTML to tell search engines — in a structured, machine-readable format — exactly what a product page is about.
It's implemented as JSON-LD (JavaScript Object Notation for Linked Data), a block of <script type="application/ld+json"> that sits in your page's <head> or <body>. Google, Bing, and other search engines parse this block and use it to display rich results — enhanced search listings that stand out from the standard blue link.
For product pages specifically, this means:
- Star ratings displayed directly in search results
- Price and currency
- Stock availability ("In Stock" / "Out of Stock")
- Review count
- Product image in some placements
Why Product Schema Matters for SEO
Let me be direct: Product Schema is not a ranking factor. Google has confirmed this. Adding structured data doesn't push your page from position 12 to position 3.
What it does is improve CTR at whatever position you already hold.
A listing showing ⭐⭐⭐⭐⭐ 847 reviews · $49 · In Stock is visually larger, more trustworthy, and more clickable than a plain text result at the same position. Studies consistently show 10–30% CTR improvements for product pages with rich results enabled.
More clicks at the same position → more engagement signals → potentially better rankings over time. The effect is indirect but real.
There's also a second benefit: Google Shopping and merchant listings. If you connect your structured data with a Google Merchant Center account, your products can appear in Shopping tabs and price comparison features — without running paid ads.
What Google Can Show: Rich Result Types for Products
Not every field you add shows up. Google selects what to display based on query context and result layout. Here's what's eligible:
| Feature | What it shows | Requires |
|---|---|---|
| Star ratings | ⭐ 4.5 (847 reviews) |
aggregateRating with ratingValue + reviewCount
|
| Price | $49.99 |
offers → price + priceCurrency
|
| Availability | "In stock" |
offers → availability
|
| Price drop | Strikethrough original price |
offers → historical price data |
| Shipping | Free shipping info |
offers → shippingDetails
|
| Returns | Return policy |
offers → hasMerchantReturnPolicy
|
| Review snippets | Individual review text |
review array |
Google doesn't guarantee showing any of these — it's at their discretion. But you can't get them at all without the markup.
The Minimum Valid Product Schema
Google's Rich Results requirements state that a Product must have name plus at least one of: review, aggregateRating, or offers. Everything else is recommended but not required.
Here's the minimum that passes Google's Rich Results Test:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ergonomic Office Chair Pro",
"offers": {
"@type": "Offer",
"price": "349.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
This is valid. It won't trigger rich results (no ratings), but it won't throw errors either. Think of it as the floor, not the target.
The Complete Product Schema: All Recommended Fields
Here's a production-ready example with every field Google recommends:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ergonomic Office Chair Pro",
"description": "Fully adjustable ergonomic office chair with lumbar support, breathable mesh back, and 5-year warranty. Supports up to 150kg.",
"image": [
"https://example.com/images/chair-front.jpg",
"https://example.com/images/chair-side.jpg",
"https://example.com/images/chair-back.jpg"
],
"sku": "CHAIR-ERG-PRO-001",
"mpn": "EP-2024-001",
"gtin13": "5901234123457",
"brand": {
"@type": "Brand",
"name": "ErgoComfort"
},
"color": "Black",
"material": "Mesh, Aluminum",
"offers": {
"@type": "Offer",
"url": "https://example.com/products/ergonomic-chair-pro",
"price": "349.00",
"priceCurrency": "USD",
"priceValidUntil": "2026-12-31",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": {
"@type": "Organization",
"name": "Your Store Name"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "243",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5"
},
"author": {
"@type": "Person",
"name": "Alex M."
},
"datePublished": "2026-03-15",
"reviewBody": "Best office chair I've owned. My back pain is completely gone after switching to this."
}
]
}
Field Reference: Required, Recommended, and Optional
| Field | Type | Priority | Notes |
|---|---|---|---|
name |
Text | Required | Product name, not brand name |
offers |
Offer | Required* | At minimum price + currency |
aggregateRating |
AggregateRating | Strongly recommended | Enables star display in SERPs |
image |
URL or array | Strongly recommended | Min 160×90px, max 1920×1080px |
description |
Text | Recommended | Don't duplicate meta description verbatim |
sku |
Text | Recommended | Your internal SKU |
gtin / gtin13
|
Text | Recommended | Barcode — helps Google identify the product globally |
brand |
Brand | Recommended | Separate @type: Brand object |
mpn |
Text | Recommended | Manufacturer part number |
color |
Text | Optional | Required for apparel rich results |
material |
Text | Optional | |
priceValidUntil |
Date | Recommended | ISO 8601 format: "2026-12-31" |
itemCondition |
URL | Recommended | NewCondition, UsedCondition, RefurbishedCondition |
review |
Review array | Optional | Enables individual review snippets |
*Required for rich results eligibility, along with aggregateRating or review
Multiple Offers: Handling Variants and Price Ranges
If your product has variants (sizes, colors) with different prices, you have two options.
Option 1: AggregateOffer — when variants have a price range:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Premium Wireless Headphones",
"offers": {
"@type": "AggregateOffer",
"lowPrice": "199.00",
"highPrice": "279.00",
"priceCurrency": "USD",
"offerCount": "3",
"availability": "https://schema.org/InStock"
}
}
Google will display something like "$199 – $279" in search results.
Option 2: Array of Offer objects — when you want to be explicit about each variant:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Premium Wireless Headphones",
"offers": [
{
"@type": "Offer",
"name": "Black — Standard",
"price": "199.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
{
"@type": "Offer",
"name": "White — Limited Edition",
"price": "279.00",
"priceCurrency": "USD",
"availability": "https://schema.org/LimitedAvailability"
}
]
}
Implementation in Next.js (App Router)
In Next.js App Router, inject JSON-LD using a <script> tag directly in the page component. Build the object server-side — JSON-LD must be present in the initial HTML for Google to parse it during crawl. Client-side rendered structured data is unreliable.
// app/products/[slug]/page.tsx
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images,
sku: product.sku,
brand: {
'@type': 'Brand',
name: product.brand,
},
offers: {
'@type': 'Offer',
price: product.price.toString(),
priceCurrency: product.currency,
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
url: `https://yourstore.com/products/${params.slug}`,
priceValidUntil: '2026-12-31',
itemCondition: 'https://schema.org/NewCondition',
},
aggregateRating: product.rating
? {
'@type': 'AggregateRating',
ratingValue: product.rating.average.toString(),
reviewCount: product.rating.count.toString(),
bestRating: '5',
}
: undefined,
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* rest of page */}
</>
)
}
Implementation in React (with react-helmet-async)
For React SPAs, use react-helmet-async to inject the script tag into <head>. Note: Google can render JavaScript, but it's a two-wave crawl process — SSR or static generation is always preferable for structured data reliability.
import { Helmet } from 'react-helmet-async'
function ProductPage({ product }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.image,
offers: {
'@type': 'Offer',
price: product.price.toString(),
priceCurrency: 'USD',
availability: product.available
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.rating.toString(),
reviewCount: product.reviewCount.toString(),
bestRating: '5',
},
}
return (
<>
<Helmet>
<script type="application/ld+json">
{JSON.stringify(jsonLd)}
</script>
</Helmet>
{/* page content */}
</>
)
}
Implementation in WordPress (without a plugin)
If you're managing a WooCommerce store and want full control without relying on Yoast or RankMath generating the schema for you, add this to your functions.php:
function add_product_schema() {
if ( ! is_product() ) return;
global $product;
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => get_the_title(),
'description' => wp_strip_all_tags( $product->get_short_description() ),
'image' => wp_get_attachment_url( $product->get_image_id() ),
'sku' => $product->get_sku(),
'offers' => [
'@type' => 'Offer',
'price' => $product->get_price(),
'priceCurrency' => get_woocommerce_currency(),
'availability' => $product->is_in_stock()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
'url' => get_permalink(),
],
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
}
add_action( 'wp_head', 'add_product_schema' );
This gives you precise control over every field instead of relying on plugin heuristics that may add properties you don't need or miss ones you do.
The 7 Most Common Product Schema Mistakes
These are the errors that pass syntax validation but fail Google's rich result requirements — the silent killers.
1. Price as a number instead of a string
// Wrong — will trigger a Google validation warning
"price": 49.99
// Correct
"price": "49.99"
Schema.org expects price as a string. A bare number causes warnings in Rich Results Test and may suppress the price display.
2. Missing priceCurrency
Price without currency is meaningless. Google requires both fields together. If priceCurrency is absent, the price won't display in rich results even if price is correctly formatted.
3. Relative URLs for availability
// Wrong — Google doesn't recognize plain text values
"availability": "InStock"
// Correct — must be a full schema.org URI
"availability": "https://schema.org/InStock"
Valid availability values: https://schema.org/InStock, https://schema.org/OutOfStock, https://schema.org/PreOrder, https://schema.org/LimitedAvailability, https://schema.org/Discontinued.
4. Fabricated aggregateRating data
Google's quality guidelines explicitly prohibit fake or incentivized reviews in structured data. Using fabricated ratings can result in rich result suppression or a manual penalty that affects your entire domain.
5. Including aggregateRating with zero reviews
If you have no reviews yet, don't include the aggregateRating block at all. An aggregateRating with reviewCount: 0 throws a validation error. Wait until you have real data.
6. priceValidUntil in the past
If your priceValidUntil date has expired, Google may suppress the price in rich results. Either update it regularly (build it into your CMS workflow) or set it to a far-future date if your pricing is stable.
7. Structured data that contradicts visible page content
The most important rule: your JSON-LD must match what's actually on the page. If the page shows $49 but JSON-LD says $39, or your page says "Out of Stock" but your schema says InStock — Google will flag it. This can result in a manual action that removes all rich results across your entire site.
Validating Your Product Schema
Three tools, used in this order:
1. Google Rich Results Test — search.google.com/test/rich-results
The official tool. Paste a URL or raw JSON-LD, see exactly which rich results are eligible and what errors exist. Always run this before deploying.
2. Schema.org Validator — validator.schema.org
Validates against the full schema.org specification. More strict than Google's tool. Use it to catch type mismatches and unknown properties that Google's tool sometimes ignores.
3. Google Search Console → Enhancements → Shopping
Shows real crawl data for your live pages after Google indexes them. Errors here are what actually affect your search performance — not just lab test results. Check this 7–14 days after deploying changes.
Validation workflow:
- Build JSON-LD locally and test with Rich Results Test
- Fix all errors and warnings (warnings matter for products)
- Deploy to production
- Request indexing in Search Console
- Check Enhancements tab after one to two weeks
When Product Schema Won't Help
Schema markup is a tool, not a shortcut. It won't move the needle if:
-
Your page has no rating data. You can add every field perfectly, but without
aggregateRatingorreview, Google won't show stars. That's the single most impactful rich result for product pages. - You're ranking on page 3+. Rich results are displayed in the listing, but no one sees page 3 results. Fix rankings first, then optimize the snippet.
- Your structured data contradicts page content. Google cross-validates JSON-LD against visible content. Mismatches get suppressed.
- You're in a YMYL category without domain authority. Health supplements, financial products, medical devices. Google applies extra scrutiny and requires demonstrated expertise and trustworthiness before showing rich results.
Summary
Product Schema Markup is one of the few technical SEO changes with a directly measurable outcome: your search listing either gets rich results or it doesn't. The implementation is straightforward, the validation tooling is free and official, and the potential CTR upside is real.
The practical checklist:
- Include
name,offers(price + currency + availability), andaggregateRatingat minimum - Use full schema.org URIs for
availabilityanditemCondition— not plain text keywords - Always validate against the Rich Results Test before deploying
- Keep
priceValidUntilcurrent — set a reminder if needed - Never put data in JSON-LD that contradicts what's visible on the page
- For Next.js: build the object server-side, not in a client component
If you want to skip the JSON construction entirely and generate valid Product Schema through a form interface — with live preview, Google eligibility check, and missing field warnings — there's a free browser-based tool here: Product Schema Generator. No signup, no backend, everything runs locally in the browser.
Found an error or have a question? Drop it in the comments — schema edge cases are genuinely interesting to dig into.
Top comments (0)