Ecommerce SEO structured data¶
ecommerce-seo-structured-data
Add product JSON-LD that matches the visible offer, including price, currency, availability, and images, without a second conflicting Product node. Use when editing product-page SEO, schema.org markup, or merchant listing data.
Version 0.3.2 · MIT · Source · Rule
Install this skill
Copy the skill and its rule into your project:
The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.
Use this skill when a product page needs schema.org data that matches what the shopper sees. Google's merchant listing and product snippet features have different required fields. Check a document with scripts/check-product-jsonld.mjs. The field list and the WooCommerce hook are in the JSON-LD notes. A passing merchant example is assets/example-product.jsonld.
Requirements below follow Google's Search Central product structured data documentation as reviewed for this skill. Rich result rules change. Re-check that documentation when a page is rejected, and do not promise a rich result.
Procedure¶
- Find existing Product markup before adding any. WooCommerce core prints Product data through
WC_Structured_Dataon the single product template. A second Product node for the same item conflicts with the first. - Change WooCommerce output with the
woocommerce_structured_data_productfilter. Add a new JSON-LD script only on a headless or custom storefront that does not already emit one. - Mark up the offer the page actually sells. A variant page uses that variant's price, currency, SKU, and availability. A parent page that spans several prices uses an
AggregateOfferonly for a product snippet, not as the sole offer for a merchant listing. - Put JSON-LD in the server-rendered HTML. Google documents that Product markup built only in the browser can make shopping crawls less reliable for price and availability.
- Run the checker in the profile you are targeting:
node scripts/check-product-jsonld.mjs --file product.json --profile merchant
node scripts/check-product-jsonld.mjs --file product.json --profile snippet
Merchant listing profile¶
The checker requires, for each Product:
nameimageas a crawlable URL, anImageObjectwithurl, or a list of thoseoffersas oneOffer, not anAggregateOfferand not an array- a numeric
pricegreater than zero, or the same underpriceSpecificationwhenoffers.priceis absent priceCurrencyas a three-letter ISO 4217 code on the same object that carries the price
If both offers.price and offers.priceSpecification.price are present, Google uses offers.price. The checker flags a non-positive offers.price in that case.
availability is recommended. Use one schema.org value such as https://schema.org/InStock or https://schema.org/OutOfStock. Do not mark an out-of-stock variant as InStock.
Product snippet profile¶
The checker requires name and at least one of review, aggregateRating, or offers.
- An
Offerneeds a numeric price andpriceCurrency. - An
AggregateOfferneeds numericlowPriceandhighPrice,priceCurrency, and a positiveofferCount. aggregateRating, when present, needs numericratingValueand eitherreviewCountorratingCount.
A snippet may describe a price range. A merchant listing, in the documentation this skill follows, needs a specific offer with a positive price.
Visible agreement¶
- The price in JSON-LD is the price on the page, in the currency the shopper would be charged, without a thousands separator inside the JSON number.
- The image URL returns the product image to an anonymous crawler.
nameanddescriptionare the language of the page. Translate the schema with the page. Do not leave English schema on a Persian page unless the visible title is English.- RTL does not change the JSON. Do not insert Persian digits into
priceorsku. Display digits in HTML. Keep ASCII in the structured data. - Do not mark up products that are not on the page. A category page is not a single Product with one offer unless the page is actually one product.
Canonical and language¶
- The page has one canonical URL, and the offer
urlmatches the page the shopper landed on, including the selected variant when the variant has its own URL. - When several languages exist, each language URL's canonical and
hreflangannotations have to agree. Structured data does not replacehreflang.
Gotchas¶
- A formatted string such as
"19,00 USD"or"۱۹.۰۰"is not a price. Use a JSON number. availability: InStockon a parent while every variant is sold out contradicts the page.- Copying a review
ratingValueyou do not display, or marking up reviews the page does not show, is not a fix for a missing offer. - Sitewide
Organizationor return-policy markup is separate from the product offer. Add it once, on the pages where it is visible, rather than inventing a return policy inside every offer.
Verification¶
- The checker exits 0 for the profile you ship.
- View source shows one Product for the item, inside server-rendered HTML.
- The price, currency, and availability match the variant on the page, including an out-of-stock variant.
- A Persian product page keeps Persian text in
nameand ASCII inpriceandpriceCurrency. - Disabling JavaScript still leaves the JSON-LD in the document.
Evaluation¶
Worked examples and the checks they must satisfy are in evals/tasks.json.
References¶
Product JSON-LD¶
One node on a WooCommerce product page¶
WooCommerce builds product structured data in WC_Structured_Data and prints it with the single product template. Filter the array before it is encoded:
add_filter(
'woocommerce_structured_data_product',
function ( $markup, $product ) {
if ( ! $product instanceof WC_Product ) {
return $markup;
}
$markup['brand'] = array(
'@type' => 'Brand',
'name' => 'Example',
);
return $markup;
},
10,
2
);
Only add fields you can see on the page. If a variable product page shows a range, do not force a single variant price into the parent markup.
For a custom storefront, emit one <script type="application/ld+json"> block from the server. Do not also emit WooCommerce's block if the theme already loaded it.
Merchant listing shape¶
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Cotton shirt",
"image": "https://shop.example/shirt.jpg",
"sku": "SHIRT-BLUE",
"description": "Short-sleeve cotton shirt.",
"offers": {
"@type": "Offer",
"url": "https://shop.example/shirt?color=blue",
"priceCurrency": "USD",
"price": 19.0,
"availability": "https://schema.org/InStock"
}
}
price is a JSON number. priceCurrency is ISO 4217. Merchant listing documentation requires a positive price. Product snippets can use AggregateOffer with lowPrice, highPrice, priceCurrency, and offerCount when the page shows a range.
Availability values¶
Use a single value:
https://schema.org/InStockhttps://schema.org/OutOfStockhttps://schema.org/PreOrderhttps://schema.org/BackOrderhttps://schema.org/LimitedAvailabilityhttps://schema.org/SoldOuthttps://schema.org/Discontinuedhttps://schema.org/InStoreOnlyhttps://schema.org/OnlineOnlyhttps://schema.org/PreSale
The short name, such as InStock, is also accepted by the checker. Map WooCommerce instock to InStock, outofstock to OutOfStock, and onbackorder to BackOrder.
Checker¶
checkProductJsonLd(data, profile) in scripts/check-product-jsonld.mjs reads a Product object or a @graph that contains one. Profiles:
merchantrequiresname,image, and one positiveOffer.snippetrequiresnameand at least one ofreview,aggregateRating, oroffers.
Warnings do not fail the process. Errors do. A warning on a missing availability still means you should set it when the page shows stock.
Breadcrumbs¶
WooCommerce can also emit BreadcrumbList. Add a breadcrumb node only when the visible breadcrumb is absent from existing structured data. Do not invent crumbs that are not on the page.
What this checker does not do¶
It does not fetch the image URL, does not call Google's rich results test, and does not validate return-policy or shipping-policy shapes. Those policies have their own required fields in Google's documentation. Add them when the shop displays that policy, and re-read the current field list before shipping them.
Evaluation¶
Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.
extend-woocommerce-schema¶
Add a brand to the product structured data WooCommerce already prints, without a second Product node.
Must include: woocommerce_structured_data_product
Must not include: second Product
Worked example
Filter the existing markup.
variant-offer¶
Mark up the blue variant that the page sells, including a numeric price and in-stock availability.
Must include: priceCurrency, InStock
Must not include: 19,00 USD