Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 030 / 100

Product structured data: a practical Technical SEO guide

Prices, availability and reviews in search results.

Expona Admin7 min read

If you're running an e-commerce platform or marketplace, you've probably noticed that some product listings appear richer in Google Search results—complete with prices, availability, and star ratings—while others show only a basic title and URL. That's the power of product structured data. Getting it right isn't just about SEO vanity; it directly impacts whether your products get approved for shopping feeds, qualify for rich results, and ultimately drive qualified traffic to your store.

After 5+ years building e-commerce and marketplace platforms, I've learned that structured data is one of those investments that feels invisible until it's missing. A single missing field or a price mismatch can knock your entire feed from Google Merchant Center approval. Let's walk through what you need to know to implement it properly.

Required basics — name, image, and offers with price, priceCurrency and availability

Every product on the web needs a foundational set of structured data fields. These aren't optional extras; they're the minimum Google requires to even consider your product for rich results in search.

The core fields are straightforward:

  • name — The product title as displayed to users
  • image — A high-quality product image (at least 100x100px, ideally 800x800px or larger)
  • offers — An object containing price, priceCurrency, and availability
  • price — The numerical value without currency symbol
  • priceCurrency — ISO 4217 code (USD, EUR, GBP, etc.)
  • availability — One of InStock, OutOfStock, PreOrder, Discontinued

Here's a minimal but complete example:

html
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Wireless Noise-Cancelling Headphones",
  "image": "https://example.com/images/headphones-model-x.jpg",
  "offers": {
    "@type": "Offer",
    "price": "199.99",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock"
  }
}

When I was building a ride-hailing marketplace backend, I saw teams skip the image URL or use low-res thumbnails. Google's rich result validator would flag them immediately. High-quality images aren't just nice to have—they're necessary for approval. I recommend storing at least one primary image per product with explicit width and height attributes in your structured data.

The availability field also matters more than you'd think. If you mark something as InStock when your inventory says otherwise, you'll get feed rejection or, worse, customer complaints when they order an unavailable item.

Keep prices in sync — Markup price must match the visible price, or listings get disapproved

This is where most developers run into trouble, and it's usually a data synchronization problem rather than a schema problem.

Google's merchant feed validator and rich result tester will reject your product if the price in your JSON-LD markup doesn't match the price shown on the product page. This isn't a suggestion—it's a hard requirement. When I audited a SaaS platform's product feeds, we found that 40% of disapprovals were due to price mismatches, often because:

  • The frontend displays a discounted price, but the markup shows the original price
  • The markup includes VAT, but the page shows a pre-tax price
  • Conversion rates haven't been applied correctly for multi-currency stores
  • The price is updated in the product database but the structured data is cached

Here's a scenario I've encountered: you're running a travel marketplace where prices fluctuate hourly. Your database updates a flight price from $299 to $269, but your CDN is caching the JSON-LD snippet for 24 hours. A crawler sees $299 in the markup but $269 on the page. Disapproval.

The fix is stricter synchronization. If your tech stack uses server-side rendering, generate the price markup on every request. If you're using static generation with edge updates, make sure your caching headers are aligned with your price update frequency:

html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Flight: Colombo to Singapore",
  "offers": {
    "@type": "Offer",
    "price": "269.00",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "priceValidUntil": "2025-01-20T23:59:59Z"
  }
}
</script>

The priceValidUntil field is optional but valuable—it tells Google when the price is no longer valid, reducing the risk of serving stale information.

Merchant listings — Shipping and return details make products eligible for richer results

If you're selling physical goods and want your product to qualify for the full suite of rich results—price, availability, shipping estimate, and return policy—you need to add shipping and return information to your structured data.

This is particularly important for competitive categories like electronics, apparel, and home goods, where rich results with shipping details see higher click-through rates because users can compare shipping costs before clicking through to your site.

Here's what you need to add to your Offer object:

html
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Laptop Stand",
  "image": "https://example.com/laptop-stand.jpg",
  "offers": {
    "@type": "Offer",
    "price": "45.00",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "shippingDetails": {
      "@type": "ShippingDeliveryTime",
      "shippingDestination": {
        "@type": "DeliveryAddress",
        "addressCountry": "US"
      },
      "shippingRate": {
        "@type": "PriceSpecification",
        "price": "10.00",
        "priceCurrency": "USD"
      },
      "shippingLabel": "Standard Shipping",
      "deliveryTime": {
        "@type": "ShippingDeliveryTime",
        "businessDays": 5
      }
    }
  },
  "returnPolicy": {
    "@type": "ReturnPolicy",
    "applicableCountry": "US",
    "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
    "merchantReturnDays": 30,
    "returnMethod": "https://schema.org/ReturnByMail",
    "returnFees": "https://schema.org/FreeReturn"
  }
}

When I was optimizing a marketplace's feed for Google Shopping, adding shipping and return details increased our eligibility for the "Compare prices" feature. Users could see our shipping cost inline with competitors, which helped us win price-sensitive segments.

One practical tip: if your shipping cost varies by destination, you'll need to generate separate Offer objects or shippingDetails blocks for each region. This adds complexity, but it's worth it if you're shipping internationally.

Variants — Use ProductGroup for sizes and colors with a shared parent

Now let's tackle one of the most confusing parts: how to structure data when a product comes in multiple sizes, colors, or other variants.

You have two approaches:

  1. Separate Product objects for each variant — Simple but repetitive; each size gets its own schema
  2. ProductGroup with Offers array — More elegant; one parent product with multiple child variants

I recommend the ProductGroup approach because it's cleaner and helps Google understand that all variants are part of the same logical product:

html
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Cotton T-Shirt",
  "image": "https://example.com/tshirt-red.jpg",
  "brand": {
    "@type": "Brand",
    "name": "ExampleBrand"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.5",
    "reviewCount": "120"
  },
  "offers": [
    {
      "@type": "Offer",
      "sku": "TSHIRT-RED-S",
      "availability": "https://schema.org/InStock",
      "price": "24.99",
      "priceCurrency": "USD",
      "size": "S",
      "color": "Red"
    },
    {
      "@type": "Offer",
      "sku": "TSHIRT-RED-M",
      "availability": "https://schema.org/InStock",
      "price": "24.99",
      "priceCurrency": "USD",
      "size": "M",
      "color": "Red"
    },
    {
      "@type": "Offer",
      "sku": "TSHIRT-RED-L",
      "availability": "https://schema.org/OutOfStock",
      "price": "24.99",
      "priceCurrency": "USD",
      "size": "L",
      "color": "Red"
    },
    {
      "@type": "Offer",
      "sku": "TSHIRT-BLUE-S",
      "availability": "https://schema.org/InStock",
      "price": "24.99",
      "priceCurrency": "USD",
      "size": "S",
      "color": "Blue"
    }
  ]
}

This structure tells Google that the Cotton T-Shirt is one product with multiple variants. Each variant has its own SKU, color, size, and availability status. If size L in red is out of stock but size S is in stock, Google can surface the available variant instead of hiding the entire product.

One important caveat: don't go overboard with variants. If your product has hundreds of variants (say, an apparel item in 50 colors and 10 sizes), generating 500 separate Offer objects will bloat your structured data and slow down your page. For massive variant sets, consider using a feed file (like Google Merchant Center's XML format) instead of inline JSON-LD.

Common mistakes

Missing or invalid image URLs. I've seen teams use relative URLs or broken image links in structured data. Google's crawler needs absolute, publicly accessible URLs. Test every image in the Rich Result Test before deploying.

Hardcoding prices instead of pulling from inventory. If your database is the source of truth for prices and availability, your structured data must query that source in real time. Hardcoding leads to drift.

Using the wrong availability enum. Use https://schema.org/InStock, not just "InStock" or "in stock". Google is strict about enum values.

Forgetting currency codes. priceCurrency: "USD" is correct. priceCurrency: "$" is not. Use ISO 4217 codes.

Mixing offer types. If you're selling the same product in multiple currencies or from different sellers, use multiple Offer objects in an array. Don't create separate Product objects unless they're truly different products.

Ignoring aggregateRating. Adding review counts and ratings is optional for basic approval, but it dramatically increases click-through rates. If you have review data, include it.

Key takeaways

  • Product structured data requires name, image, and an offer with price, priceCurrency, and availability—these are non-negotiable for rich results.
  • Keep markup prices in sync with visible prices; mismatches trigger feed disapprovals and erode user trust.
  • Add shipping and return details to qualify for richer, more competitive search results in merchant feeds.
  • Use ProductGroup with an Offer array for variants instead of duplicating entire Product objects.
  • Test every product in Google's Rich Result Test before going live; this catches 90% of validation errors.
  • Pull prices and availability from your live inventory system, not static values, to avoid stale data.
  • Include reviews and ratings if available; they significantly boost click-through rates in search results.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Product structured data | Technical SEO Guide | Expona