Product Schema for Shopify Hydrogen: Implementation Guide
Implement server-rendered Product and Offer JSON-LD in Shopify Hydrogen, including variants, Markets, shipping, returns, ratings, and validation.
Product schema in Shopify Hydrogen is code you own. Your Shopify catalog supplies the facts, but your product route must turn those facts into valid, server-rendered JSON-LD that agrees with the storefront customers see.
The short answer: generate Product and Offer data from the same server-side Storefront API response used by the product page. Include the active market's price, currency, availability, product identifiers, images, brand, and canonical URL. Then render it through Hydrogen's getSeoMeta utility or an equivalent server-rendered JSON-LD script.
This guide shows the implementation pattern, the decisions that change for variants and international stores, and the checks that prevent structured data from becoming stale or misleading.
Product schema can make a page eligible for Google product and merchant experiences. It does not guarantee a rich result, a ranking, or a recommendation from an AI system.
Why Hydrogen Needs an Explicit Schema Implementation
Shopify themes provide several commerce SEO defaults. Hydrogen gives you control over the entire document, which means your team decides what structured data exists and where it is rendered.
That control is useful when a store has:
- Market-specific prices and availability
- Complex variants or product configurators
- A separate CMS
- Custom review data
- Product-specific shipping rules
- A storefront URL structure that differs from Shopify's Online Store
It also creates failure modes. A product page can show one price while its JSON-LD contains another, or render complete product data in the browser while the initial HTML contains only a loading shell.
Before implementing this guide, run the broader Shopify Hydrogen AI search audit. Schema cannot compensate for blocked pages, incorrect canonicals, or missing server-rendered content.
The Data Flow to Aim For
Use one server-side data source for the visible buy box and structured data:
- Read the request's market and selected product options.
- Query Shopify's Storefront API in the route loader.
- Select the same variant used by the buy box.
- Build page metadata and JSON-LD from that response.
- Render the product page and schema in the initial HTML.
- Validate the deployed URL, not only a local object.
Do not fetch schema data through a second client-side request. Two requests introduce unnecessary timing, caching, and consistency problems.
Query the Fields Your Schema Needs
Your product query should include the facts you intend to publish. A simplified selection looks like this:
fragment ProductSchemaFields on Product {
id
handle
title
description
vendor
seo {
title
description
}
featuredImage {
url
altText
width
height
}
images(first: 5) {
nodes {
url
}
}
selectedOrFirstAvailableVariant(
selectedOptions: $selectedOptions
ignoreUnknownOptions: true
caseInsensitiveMatch: true
) {
id
title
sku
barcode
availableForSale
price {
amount
currencyCode
}
}
}Keep the query aligned with the active Hydrogen and Storefront API versions in your project. Generated Storefront API types should be the source of truth for the exact response shape.
If reviews, shipping terms, or custom attributes come from another system, fetch them on the server and only include them when they are also visible to customers.
Build a Product JSON-LD Object
The builder below focuses on a product page with one active offer. Adapt its types to your generated Storefront API types.
type Money = {
amount: string;
currencyCode: string;
};
type VariantForSchema = {
sku?: string | null;
barcode?: string | null;
availableForSale: boolean;
price: Money;
};
type ProductForSchema = {
handle: string;
title: string;
description: string;
vendor?: string | null;
featuredImage?: {url: string} | null;
images: {nodes: Array<{url: string}>};
selectedOrFirstAvailableVariant?: VariantForSchema | null;
};
export function buildProductJsonLd({
product,
canonicalUrl,
}: {
product: ProductForSchema;
canonicalUrl: string;
}) {
const variant = product.selectedOrFirstAvailableVariant;
const images = product.images.nodes.map((image) => image.url);
if (product.featuredImage?.url && !images.includes(product.featuredImage.url)) {
images.unshift(product.featuredImage.url);
}
return {
"@context": "https://schema.org",
"@type": "Product",
"@id": `${canonicalUrl}#product`,
name: product.title,
description: product.description,
url: canonicalUrl,
image: images,
brand: product.vendor
? {
"@type": "Brand",
name: product.vendor,
}
: undefined,
sku: variant?.sku || undefined,
gtin: variant?.barcode || undefined,
offers: variant
? {
"@type": "Offer",
url: canonicalUrl,
price: variant.price.amount,
priceCurrency: variant.price.currencyCode,
availability: variant.availableForSale
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
itemCondition: "https://schema.org/NewCondition",
}
: undefined,
};
}Important details:
- Keep prices as the Storefront API returns them. Do not parse and reformat the amount for JSON-LD.
- Use the active market's currency.
- Omit optional values that do not exist instead of inventing placeholders.
- A barcode is not automatically a GTIN in every catalog. Confirm how identifiers are managed before mapping it.
- Change
itemConditionif the merchant sells used or refurbished goods. - Use absolute image and product URLs.
Connect JSON-LD to the Hydrogen Route
Hydrogen's current recommendation is getSeoMeta. Its older Seo component is deprecated.
A simplified product route can return one SEO object from its loader:
import {getSelectedProductOptions, getSeoMeta} from "@shopify/hydrogen";
import type {MetaFunction} from "react-router";
export async function loader({context, params, request}: Route.LoaderArgs) {
const selectedOptions = getSelectedProductOptions(request);
const {product} = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.handle,
selectedOptions,
},
});
if (!product) {
throw new Response("Product not found", {status: 404});
}
const requestUrl = new URL(request.url);
const canonicalUrl = `${requestUrl.origin}/products/${product.handle}`;
const jsonLd = buildProductJsonLd({product, canonicalUrl});
return {
product,
seo: {
title: product.seo.title || product.title,
description: product.seo.description || product.description,
url: canonicalUrl,
media: product.featuredImage
? {
type: "image",
url: product.featuredImage.url,
altText: product.featuredImage.altText || product.title,
width: product.featuredImage.width,
height: product.featuredImage.height,
}
: undefined,
jsonLd,
},
};
}
export const meta: MetaFunction<typeof loader> = ({data, matches}) => {
return getSeoMeta((matches[0] as {data?: {seo?: object}}).data?.seo, data?.seo);
};The exact route arguments and helper imports vary between Hydrogen versions. The architectural requirement does not: the loader should return the product and its SEO data together, and the meta export should merge route-specific metadata after parent metadata.
getSeoMeta preserves JSON-LD from each route while later SEO objects override fields such as title and description. This lets a root route own Organization data while the product route owns Product data.
Confirm the JSON-LD Is Server-Rendered
After deployment:
- Open a public product URL.
- View the raw page source or fetch the URL without running browser JavaScript.
- Search for
application/ld+json. - Confirm the Product object contains the current price and availability.
- Compare the raw response with the visible buy box.
Seeing schema in browser developer tools is not enough. Developer tools show the live DOM after scripts have executed; the raw response shows what a crawler receives first.
Google recommends including merchant listing Product data in the initial HTML.
Handling Product Variants
Variant modeling depends on the storefront's URL and content behavior.
One product URL with a selected variant
When all options live on one product URL, emit the offer that matches the currently selected or first available variant. The visible price, availability, SKU, and structured offer must describe the same selection.
If changing an option updates only the browser state, the server-rendered schema will still describe the initial selection. That is acceptable when the canonical page represents the product as a whole, but do not claim every variant shares the same price or availability.
Stable URLs for individual variants
If variants have crawlable URLs with distinct content, each URL should:
- Resolve without client-side navigation
- Have a self-consistent canonical strategy
- Render the selected variant in the initial HTML
- Emit that variant's price, SKU, identifiers, images, and availability
- Avoid creating indexable URLs for meaningless option combinations
ProductGroup for variant families
Google supports ProductGroup with hasVariant, variesBy, and productGroupID. This is useful for a true variant family, but it is more complex than adding multiple offers to a generic Product.
A reduced shape looks like:
{
"@context": "https://schema.org",
"@type": "ProductGroup",
"name": "Everyday Cotton Shirt",
"productGroupID": "SHIRT-100",
"variesBy": [
"https://schema.org/color",
"https://schema.org/size"
],
"hasVariant": [
{
"@type": "Product",
"sku": "SHIRT-100-BLK-M",
"color": "Black",
"size": "M",
"offers": {
"@type": "Offer",
"price": "79.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
}
]
}Do not copy this object unchanged. Populate every variant from current catalog data and follow Google's separate guidance for single-page or multi-page variant sites.
Shopify Markets and Localized Offers
Structured data must reflect the market represented by the URL.
For each localized product page, verify:
priceCurrencymatches the visible currencypricematches the visible market price- Availability reflects that market
- The canonical URL belongs to the current locale
- Images and product copy match the localized page
hreflangconnects valid market alternatives
Do not serve a cached USD offer on a Canadian-dollar page. Cache keys must account for the context that changes product output, including country, language, currency, and customer state where relevant.
If currency changes entirely on the client without a market-specific URL, decide which market the canonical URL represents and keep its initial HTML internally consistent.
Shipping and Return Policies
For standard business-wide terms, Google recommends publishing shipping and return policy data under Organization markup rather than repeating full policies inside every product.
Use:
hasShippingServicewithShippingServicefor standard shipping policyhasMerchantReturnPolicywithMerchantReturnPolicyfor standard returns
Place the markup on the page that explains the policy and connect it to the merchant's Organization entity.
Use offer-level shippingDetails or hasMerchantReturnPolicy only when a product needs to override the standard policy. The supported offer-level fields are a smaller subset.
Never add a free-shipping or 30-day-return example unless that is the merchant's real, visible policy for the stated country.
Ratings and Reviews
Add aggregateRating only when the same first-party review information appears on the product page.
const aggregateRating =
reviewSummary && reviewSummary.count > 0
? {
"@type": "AggregateRating",
ratingValue: reviewSummary.average,
reviewCount: reviewSummary.count,
}
: undefined;Common mistakes include:
- Adding a store-wide rating to every product
- Rendering reviews only after a client request while schema claims they exist
- Caching an old review count
- Marking up testimonials that are not product reviews
- Including ratings outside the provider's permitted usage
Fetch review summaries on the server when they are part of the structured Product object, or leave the rating out until the integration can remain accurate.
Product Schema and Google Merchant Center
Product JSON-LD and Merchant Center feeds should reinforce each other.
Compare these values regularly:
- Product and variant identifiers
- Canonical links
- Prices and sale prices
- Currency
- Availability
- Brand and GTIN
- Images
- Shipping and returns
Google can use both sources to understand and verify product information. A feed/schema mismatch is not an AI SEO opportunity; it is a catalog quality problem.
Does Product Schema Make a Store Discoverable by AI?
Structured data helps systems identify explicit product facts, but there is no special AI-search Product schema.
AI visibility still depends on:
- Crawlable server-rendered product pages
- Accurate catalog attributes
- Clear product descriptions
- Consistent merchant identity
- Trustworthy shipping, return, and review information
- Merchant Center and storefront agreement
- Useful editorial content that answers buyer questions
For Shopify's agentic commerce interfaces, catalog quality also affects how well products match natural-language requests. Use consistent metafields for material, fit, compatibility, dimensions, use case, and other decision-making attributes instead of hiding those facts in inconsistent prose.
Validation Workflow
Validate a representative set of pages rather than testing one easy product.
Include:
- A product with one variant
- A product with several options
- An out-of-stock product
- A sale product
- A product with reviews
- A product without identifiers
- One URL from every important market
For each page:
- Compare visible content with raw HTML.
- Run Google's Rich Results Test.
- Run Schema.org's validator for vocabulary issues.
- Inspect Merchant listings and Product snippets reports in Search Console.
- Compare the page with its Merchant Center item.
- Re-test after changing pricing, review, localization, or product-route code.
Add automated assertions for the schema builder. At minimum, test that an in-stock and out-of-stock variant produce the correct availability, and that missing optional fields are omitted rather than serialized as empty strings.
Production Checklist
- Product JSON-LD appears in the initial HTML
- Name and description match the page
- Canonical and Offer URLs use the production domain
- Images use absolute crawlable URLs
- Brand is accurate
- SKU and GTIN are included only when valid
- Price and currency match the active market
- Availability matches the selected variant
- Reviews are visible, genuine, and current
- Shipping and return data match published policies
- Variant modeling matches the URL strategy
- Merchant Center and storefront values agree
- Representative products pass validation
- Search Console enhancement reports are monitored
When to Fix the Catalog Instead of the Schema
Schema should describe the catalog, not repair it.
Fix the source data when:
- Variant SKUs are missing or duplicated
- Barcodes are unreliable
- Product vendors are inconsistent
- Attributes use different metafields across products
- Market prices are not synchronized
- Shipping rules exist only in team knowledge
A clean catalog improves storefront filters, feeds, search, analytics, and AI shopping integrations at the same time.
Need Help Implementing It?
We build Shopify Hydrogen storefronts and custom headless Shopify experiences, including product routes, Markets, structured data, performance, and migration safeguards.
If you want an implementation review before launch, contact Webmakers Studio. We will compare the rendered storefront, raw HTML, Shopify catalog, Merchant Center, and market behavior rather than reviewing an isolated schema snippet.