The 15 signals we test
The free CatalogScan scan reads your storefront's public surfaces and scores 15 machine-readable signals that AI shopping agents — ChatGPT Shopping, Perplexity Shopping, Google AI Mode, Shopify's Global Catalog — use to decide whether to surface your products. This page explains every signal: what it is, why agents read it, how to test it yourself, and how to fix it.
The 15 signals
- Public product feed floor · 25 pts
- Product JSON-LD on PDP floor · 30 pts
- Sitemap floor · 15 pts
- Open Graph tags floor · 15 pts
- Robots not blocking floor · 15 pts
- GTIN coverage across variants deep · 10 pts
- Review schema (AggregateRating) deep · 10 pts
- Brand entity in JSON-LD deep · 8 pts
- Canonical URL on PDP deep · 8 pts
- Description richness deep · 8 pts
- Offers availability deep · 6 pts
- SKU coverage across variants deep · 6 pts
- Image alt-text coverage deep · 6 pts
- Hreflang on PDP deep · 4 pts
- Structured data validity deep · 4 pts
Floor signals 5 signals · 100 pts · can't skip
Every AI shopping agent needs these five. Miss any one and you're invisible to that agent on that dimension, no matter how strong the rest of your catalog is. A default Shopify storefront gives you all five out of the box; headless stores frequently drop one or two during the cutover.
/products.json — the single most-ingested surface on your store. Returns up to 250 products per call with standard ?page=N&limit=N pagination and includes handles, titles, descriptions, vendor, product type, variants, options, images, and inventory flags.curl -s https://yourstore.com/products.json?limit=1. Expect a JSON body with a products array. A 404, an HTML login page, or plain HTML means the endpoint is dead on your storefront.storefront_password. On headless (Hydrogen, Next.js + Storefront API, custom stacks): you lost the endpoint during the cutover — re-expose it by proxying /products.json back to Shopify, or ship a custom endpoint that emits the same shape. In our 100-store scan, 40% of headless DTC brands dropped this endpoint entirely.<script type="application/ld+json"> block on every product detail page with @type: "Product" describing price, availability, brand, GTIN, SKU, reviews, and description in machine-readable form.application/ld+json, confirm a block where @type equals Product. Validate it via Google's Rich Results Test.@type: "Product", you're getting zero credit — the block has to be a Product, not just an Organization or WebPage. Fix in your theme's product.liquid template./sitemap.xml listing every canonical URL on your store. Either a flat <urlset> or a <sitemapindex> with sub-sitemaps per content type.curl -s https://yourstore.com/sitemap.xml | head -5. Expect <?xml> plus <urlset> or <sitemapindex>.<head>: og:title, og:description, og:image. Partial credit: 5 pts per tag present.og:title, og:description, og:image. The image should be at least 1200×630 and actually representative of the brand.theme.liquid's <head>. Most Shopify themes have them for PDPs but not the homepage. A one-liner per tag./robots.txt that doesn't blanket-block catalog paths for User-agent: *. Specifically: no Disallow: /, Disallow: /products, Disallow: /products.json, or Disallow: /collections.robots.txt. One wrong Disallow line renders every other signal moot — you can have perfect JSON-LD and it won't matter because the bot doesn't fetch the page. This is the most common "I got invisible overnight" bug we see.curl -s https://yourstore.com/robots.txt. Confirm no blanket Disallow inside the User-agent: * block. Disallow: /*?sort_by is fine (it only blocks sort-permutation URLs).Disallow: / or Disallow: /products. If your store went private while testing and you forgot to flip it back, that's also this signal.Ranking-spread signals 10 signals · 70 pts · decides placement
These are what AI agents use to rank the stores that already cleared the floor. When 20 Shopify stores all sell the same-ish t-shirt, these 10 signals are how the agent decides which one to lead with. Every one of them is checkable from public data — no Shopify login required.
variants[].barcode. Full credit at ≥90% of sampled variants having a barcode; half credit 50–89%.curl -s https://yourstore.com/products.json | jq '[.products[].variants[].barcode] | length as $t | map(select(. != null and . != "")) | length as $f | "\($f)/\($t)"'. Target ≥90%.Barcode column; (c) if you don't have GTINs, buy them from GS1 — a one-time per-SKU fee, usable across retailers. Our Pro tier auto-enriches via public GS1 lookups before you pay.aggregateRating node in your Product JSON-LD, with ratingValue and reviewCount. Either inline on the Product or as a separate AggregateRating graph node.aggregateRating node = no social proof input to the ranker. In our 100-store scan, ~9 out of every 10 stores was missing this.aggregateRating. If the tag's missing but Judge.me / Yotpo / Loox / Stamped etc. is installed, the review app isn't injecting it.brand property on your Product JSON-LD, ideally nested as {"@type": "Brand", "name": "YourBrand"}. A plain string brand gets half credit; a missing brand gets zero."brand". A string ("brand": "Allbirds") gets 4 pts; a nested entity ("brand": { "@type": "Brand", "name": "Allbirds" }) gets 8.brand as a string pulled from the Vendor field. Update your theme's product.liquid schema block to emit the nested form — single commit, same data.<link rel="canonical"> tag on every PDP pointing at the product's absolute canonical URL. Absolute URL = full credit; relative (/products/foo) = half credit; missing = zero./products/foo), collection-scoped (/collections/x/products/foo), tracking-param-decorated — and agents need a single trusted URL to consolidate ranking signals against. Missing canonicals split your authority across variants, and deduping across stores becomes impossible.rel="canonical". The href should be an absolute https:// URL, not a relative path.theme.liquid: <link rel="canonical" href="{{ canonical_url }}">. If your theme emits a relative canonical, change to {{ shop.url }}{{ canonical_url }}.curl -s https://yourstore.com/products.json | jq '.products[].body_html' | awk '{ gsub(/<[^>]*>/, ""); print NF }' — find the median.availability field on your Product JSON-LD's offers node, pointing to a schema.org vocabulary URL like https://schema.org/InStock. Full credit for the full URL; half credit for the short form (InStock); zero if missing or malformed.availability field, the agent has no way to know stock status and your product gets down-ranked for safety.offers object in your Product JSON-LD, check the availability value."availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}". Strict parsers reject the short form.curl -s https://yourstore.com/products.json | jq '[.products[].variants[].sku] | length as $t | map(select(. != null and . != "")) | length as $f | "\($f)/\($t)"'.images[].alt in the feed. Full credit ≥80% of sampled images have non-empty alt; half credit 30–79%.curl -s https://yourstore.com/products.json | jq '[.products[].images[].alt] | length as $t | map(select(. != null and . != "")) | length as $f | "\($f)/\($t)"'.<link rel="alternate" hreflang="…"> tags on your PDP mapping each region-specific version of a product page. Full credit for 2+ locales; half credit for 1.hreflang=. Count unique locale tags.<link rel="alternate"> per store, pointing at the region-equivalent product URL. Don't forget the x-default fallback.application/ld+json block on your PDP parses as valid JSON. If you have 3 JSON-LD blocks and 1 throws a parse error, you score 0 — agents skip the whole script tag on syntax error, including the valid ones before the break.<script type="application/ld+json"> block's contents into Google's Rich Results Test. Any red X = one invalid block.{{ description | strip_html | json }} — instead of manual string concatenation. When you upgrade your theme, re-test.See also
- Agentic Storefronts checklist — the full 18-signal reference (the 3 Admin-API-gated signals are Pro-only and not scanned here)
- Shopify + ChatGPT Shopping — what March 24 did + the 8 things you still own
- Perplexity Shopping setup — Brand-entity and canonical weights
- The 100 DTC Shopify stores scored against these signals
Which of the 15 are you failing?
Free 2-minute scan. Paste your store URL, get a color-graded scorecard with every signal checked inline.
Scan my store → See pricing