Skip to main content
Field NotesShopify Signals10 min read

Why your Schema.org is broken (prove it in 10 min)

Seven out of ten Shopify PDPs we audit ship with Schema.org errors that block the rich result. The same errors block AI citations. Here are the twelve most common ones, the fixes, and the four-layer validation stack that makes sure they don't come back after an app update.

Harry Parker
Co-founder, Onviqa Inc. · Surfient
schema-validator.svg
TL;DR
  • Seven of twelve common Schema.org errors ship on the median Shopify PDP — each one blocks the rich result and silently kills AI citations.
  • The most expensive errors are invalid availability enums, missing priceValidUntil, and duplicate Product nodes from review apps.
  • A four-layer validation stack (Rich Results → validator.schema.org → custom unit tests → live cron) catches 100% of regressions.

We audit Shopify Schema.org output for a living. Out of every ten PDPs we crawl, seven ship with at least one error that blocks the rich result — and therefore degrades AI citation share. Most merchants don't know because the errors don't throw, they silently return a downgraded rich result or no snippet at all. This post is the twelve errors we see most, the one-line fix for each, and the validation stack that keeps them from coming back after the next app update.

The problem with ship-and-forget

Schema.org output on Shopify is usually shipped once during theme setup, validated by one pass through Google's Rich Results Test, and then forgotten. The problem: schema regresses constantly. An app install adds duplicate Product nodes. A theme update changes how brand is emitted. A metafield rename breaks the additionalProperty map. A reviewCount drops to zero after a store reset. None of these throw an error in production. All of them silently degrade or hide the rich result.

The fix is never "write correct schema." You already did that. The fix is a validation stack that runs continuously and catches regressions before they land.

The twelve errors we see on 70% of Shopify PDPs

This list is ranked by impact on AI citation share based on our Q1 2026 audit dataset (131 Shopify stores). The first five are the ones we fix first on every engagement.

Table of twelve common Schema.org errors on Shopify product pages with symptom in the Google Rich Results tool and the merchant-side fix for each one.
Figure 1 — The twelve Schema.org errors we find on seven out of ten Shopify product pages, with the Rich Results symptom and the merchant-side fix.

1. Missing Offer.priceValidUntil

Google requires priceValidUntil on Product offers since the 2023 guideline update. Merchants who set up schema earlier often skip it. The Rich Results warning is "Missing field priceValidUntil (optional)." The word "optional" is misleading — without it, the offer ships but with reduced eligibility for the price-drop rich result. Fix: emit today+30d on every render. Never a static date.

2. Invalid Offer.availability enum

Every Shopify theme we audit has at least one product page emitting free-text values like "In Stock," "Out of Stock," or "Pre-order" for availability. These are not valid. The spec expects the full enum URL. Fix:

// Wrong
"availability": "In Stock"

// Right
"availability": "https://schema.org/InStock"

3. Product.brand as a string

Another 2023+ tightening: brand must be an object with @type: Brand, not a raw string. Shopify's default product schema snippet ships the string form. Fix:

"brand": {
  "@type": "Brand",
  "name": "Alora"
}

4. aggregateRating.reviewCount equals zero

Review apps default to emitting aggregateRating on every product — even products with zero reviews. Google's logic: if reviewCount is zero, silently hide the entire rich result rather than risk showing an empty rating. Fix: suppress the aggregateRating object entirely when reviewCount === 0. In Liquid:

{% if product.metafields.reviews.rating_count > 0 %}
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": {{ product.metafields.reviews.rating }},
    "reviewCount": {{ product.metafields.reviews.rating_count }}
  },
{% endif %}

5. Relative image URLs

Shopify's CDN serves images at paths like /cdn/shop/files/alora-hero.jpg. Emitting that relative path into Product.image means Google (and GPTBot, and Perplexity) can't resolve it without the domain. Fix: prefix with the canonical domain on every emission.

6. Duplicate Product nodes

The most frustrating error because it's invisible in DevTools unless you grep. An example we see weekly: theme emits a full Product node, and a review app (often Judge.me, Yotpo, or Okendo with default settings) emits its own stripped-down Product node to attach reviews. Google picks whichever it parses last. Fix: view-source, grep for type":"Product, count the results. If > 1, disable one source.

7. Missing Offer.shippingDetails

Agents (ChatGPT Atlas, Perplexity Pro, Claude with browser) now check Offer.shippingDetails.eligibleRegion before attempting cart-add. Missing it means the agent aborts and recommends a competitor with fuller schema. Fix:

"shippingDetails": {
  "@type": "OfferShippingDetails",
  "shippingRate": {
    "@type": "MonetaryAmount",
    "value": "0",
    "currency": "USD"
  },
  "shippingDestination": {
    "@type": "DefinedRegion",
    "addressCountry": "US"
  },
  "deliveryTime": {
    "@type": "ShippingDeliveryTime",
    "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 1, "unitCode": "DAY" },
    "transitTime": { "@type": "QuantitativeValue", "minValue": 2, "maxValue": 5, "unitCode": "DAY" }
  }
}

8. Review without itemReviewed back-reference

Review nodes often emit in isolation — just the review content, author, and datePublished — without linking back to the Product. Google treats these as orphans. Fix: every Review node gets itemReviewed: { "@id": productId }.

9. Relative URLs in BreadcrumbList

Same issue as error 5 but on breadcrumbs. listitem.item must be an absolute URL or the whole breadcrumb chain silently fails. Fix: template your breadcrumbs with the canonical domain prefix.

10. WebSite without @id

Advanced-graph stores that emit Organization, WebSite, and Product nodes expecting them to link often forget to give WebSite a stable @id. Without it, the graph breaks and Organization.sameAs references go nowhere. Fix: always @id: "https://domain.com#website".

11. Stale priceValidUntil

We've seen stores shipping priceValidUntil: "2024-01-01" in 2026. The date was hardcoded during initial setup and never regenerated. Rich Results warns, AI engines notice. Fix: generate on every build. In Hydrogen loader, return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10).

12. Single Offer for multiple variants

When a product has variants (sizes, colors, finishes), offers must be an array of Offer objects — one per variant — not a single Offer. Single-Offer emission means agents see only one price, usually the cheapest or the first variant, and can't fulfill buy commands for the others. Fix: iterate variants and emit one Offer per variant with a stable sku keyed to the Shopify variant ID.

The four-layer validation stack

Fixing the twelve errors once is a theme change. Keeping them fixed forever is where most merchants regress. The stack below is what our highest-performing clients (90%+ citation share, 95+ QPA scores) run in production.

Four-layer validation stack for Schema.org on Shopify showing the percentage of errors each layer catches.
Figure 2 — Four-layer validation stack: Rich Results Test catches 60% of errors, Schema.org Validator catches 80%, custom unit tests catch 95%, live monitoring catches 100%.

Layer 1 — Google Rich Results Test (manual, one-off)

The default starting point. Paste a URL into search.google.com/test/rich-results and read the report. Catches ~60% of the twelve: syntax, missing required fields, invalid enums. Misses duplicate nodes and cross-node references. Run this on every PDP template after any theme change.

Layer 2 — Schema.org Validator (stricter)

validator.schema.org runs the spec strictly. Catches everything Rich Results catches plus cross-node reference failures, orphan Reviews, BreadcrumbList URL resolution, @id linkage. Coverage: ~80%. Run after Layer 1 passes.

Layer 3 — Custom unit tests in CI

This is where most merchants stop, and it's where the highest-leverage fixes live. A small Vitest (or Jest) test that fetches your PDP via HTTP, parses the JSON-LD block, and asserts shape against a JSON schema. Example:

import { describe, it, expect } from "vitest";
import Ajv from "ajv";
import { productSchemaJSON } from "./schemas/product.json";

const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(productSchemaJSON);

describe("PDP JSON-LD", () => {
  it("alora-72 matches Product schema", async () => {
    const html = await fetch("https://yourshop.com/products/alora-72").then((r) => r.text());
    const ld = extractJSONLD(html);
    expect(validate(ld)).toBe(true);
    expect(ld.offers).toHaveLength(6); // 6 variants
    expect(ld.aggregateRating.reviewCount).toBeGreaterThan(0);
    expect(ld.brand["@type"]).toBe("Brand");
    expect(ld.offers[0].priceValidUntil).toMatch(/^\d{4}-\d{2}-\d{2}$/);
  });
});

Add this to your CI on every PR. The test breaks when a regression lands; no one merges until fixed. Coverage: ~95%.

Layer 4 — Live URL monitoring (weekly cron)

CI catches what developers break. It doesn't catch what ops breaks — app installs, metafield edits, theme-settings flips. Layer 4 is a scheduled cron that runs weekly against production URLs, hashes the JSON-LD, diffs vs. the baseline hash, and Slacks on drift.

This is where Surfient fits — our platform runs this weekly for every client and the drift alerts are how we catch 60% of the regressions. You can build it yourself in 30 lines of Node if you don't need the full platform.

The ten-minute audit you can run right now

  • Open your top-revenue PDP and paste it into Google's Rich Results Test. Note every warning and error.
  • Paste the same URL's raw JSON-LD into validator.schema.org. Compare findings.
  • View-source, grep for type":"Product — if you see more than one match, you have duplicate nodes.
  • Check Offer.availability — if it's a string like "In Stock" instead of a URL, it's invalid.
  • Check Offer.priceValidUntil — is it missing or in the past? Both block the rich result.
  • Check Product.brand — is it a string or an object? Only the object form is valid.
  • Check aggregateRating.reviewCount — if it's 0 on a product with no reviews, suppress the whole aggregateRating node.
  • Check Product.image URLs — absolute or relative? They must be absolute.
  • If you find any of the twelve errors in this post, you have a schema bug. Fix it, re-run step 1, confirm green.
Tags:Schema.orgJSON-LDValidationTechnical

Frequently asked questions

Try Surfient free

See how your Shopify store scores with AI engines

Surfient audits every signal ChatGPT, Perplexity, Claude, and Google AI Overviews read on your store — in under 60 seconds, with no install, no card, no catch.

  • ChatGPT, Perplexity, Claude, and AI Overviews
  • Store-by-store score with fix priorities
  • 60-second audit, no install or card
Harry Parker
Co-founder, Onviqa Inc. · Surfient

Harry has led SEO and e-commerce engineering for over 12 years and has been shipping Shopify software since Onviqa was founded in 2014. He writes about where commerce is headed when shoppers stop typing queries and start asking assistants.

Related reading

All posts