An ecommerce site has a long-tail SEO problem that a brochure site doesn't. Your homepage and three category pages probably look fine. Your 4,200 product pages — generated from a CMS template, with thumbnails of varying weight, with alt text that may or may not have been filled in, with structured data that the theme may or may not be emitting — are the long tail. Most of them are bad in ways nobody on the team has looked at. Some of them rank.
This post is about the workflow we recommend for a Shopify, WooCommerce, or Magento store with 1,000+ products: a weekly batch audit of the entire catalog, surfaced as a triage queue, with historical tracking so you can see what theme update broke what.
What goes wrong on product pages specifically
The audit checks that matter most for ecommerce, in our experience:
- Product schema (
Product+Offer) — emitted or missing. A surprising number of themes only emit it on certain product types or conditionally based on inventory. alttext on the gallery images. Shopify defaults to product title, which is fine. Custom themes routinely strip this. Accessibility tanks silently.titletag length. Templates that prepend the store name push longer product titles past 60 chars; Google truncates.- Canonical correctness on variants.
/products/sneaker?variant=42should canonicalize to/products/sneaker. Half the themes we audit don't. - Performance on the gallery. Above-the-fold image weight is the LCP problem on every product page that has it.
- Description content depth. Pages with one-paragraph descriptions hit the AI readability floor and never rank against a competitor with proper specs + use cases + FAQs.
None of these are visible from the homepage. None of them are caught by Google Search Console at the SKU level until they're already hurting impressions.
The weekly batch workflow
The basic loop is the same as the sitemap-driven audit, but with a few ecommerce-specific tweaks: pull the sitemap_products_*.xml (Shopify's pattern), sample the catalog if it's huge, and tag results by category for triage.
import os, csv, requests, xml.etree.ElementTree as ET
from collections import defaultdict
API_KEY = os.environ["SEOSCORE_API_KEY"]
STORE = "https://yourstore.com"
BASE = "https://api.seoscoreapi.com"
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
# Shopify publishes one product sitemap per 5k products
def fetch_products():
idx = requests.get(f"{STORE}/sitemap.xml").text
root = ET.fromstring(idx)
product_sitemaps = [
loc.text for loc in root.findall(".//sm:sitemap/sm:loc", ns)
if "products" in loc.text
]
urls = []
for sm in product_sitemaps:
sub = ET.fromstring(requests.get(sm).text)
urls.extend(l.text for l in sub.findall(".//sm:url/sm:loc", ns))
return urls
urls = fetch_products()
print(f"Auditing {len(urls)} products")
results = []
for i in range(0, len(urls), 50):
r = requests.post(
f"{BASE}/audit/batch",
headers={"X-API-Key": API_KEY},
json={"urls": urls[i:i+50]},
timeout=240,
)
r.raise_for_status()
results.extend(r.json()["results"])
For Shopify specifically, sitemap_products_1.xml exists and is a clean source of truth. For WooCommerce, it's whatever your SEO plugin emits (Yoast: product-sitemap.xml). For Magento, the built-in sitemap generator publishes sitemap.xml with <changefreq> set per resource type — easy to filter.
Sampling when the catalog is huge
A 50,000-SKU catalog is not something you audit in one run on a monthly plan. Two reasonable patterns:
Stratified sample. Group products by category or collection. Audit a fixed sample from each:
import random
from urllib.parse import urlparse
by_category = defaultdict(list)
for u in urls:
# /products/shoes-sneaker-blue → category = "shoes"
parts = urlparse(u).path.strip("/").split("/")
category = parts[1].split("-")[0] if len(parts) > 1 else "other"
by_category[category].append(u)
sampled = []
for cat, group in by_category.items():
sampled.extend(random.sample(group, min(50, len(group))))
This gives you good coverage of categories (every collection has at least some pages audited) for ~1/10th the audit budget.
Bestsellers first. Pull your top 500 products by revenue from the backend, audit those weekly, audit the long tail monthly. This is what most large stores end up doing — your top products do the vast majority of organic traffic, and a regression there is what costs you money.
The triage CSV
The output we hand to clients on every weekly run:
rows = []
for r in results:
rows.append({
"url": r["url"],
"score": r.get("score", 0),
"grade": r.get("grade", "F"),
"perf": r.get("categories", {}).get("performance", 0),
"a11y": r.get("categories", {}).get("accessibility", 0),
"seo": r.get("categories", {}).get("seo", 0),
# Per-product flags
"has_product_schema": any(
c.get("name") == "structured_data_product" and c.get("pass")
for c in r.get("checks", [])
),
"has_canonical": any(
c.get("name") == "canonical_tag" and c.get("pass")
for c in r.get("checks", [])
),
"issues": len(r.get("priority", [])),
})
rows.sort(key=lambda r: (r["score"], -r["issues"]))
with open("catalog-audit.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
Open it in a spreadsheet, filter for has_product_schema=False, and that's the first conversation with your developer. Filter for perf < 60, that's the gallery/image conversation. Filter for a11y < 70, that's the ADA conversation (see the ecommerce-adjacent accessibility posts for how serious that's getting in regulated verticals).
What historical tracking adds
The single most useful question on an ecommerce site is "what did the last theme update break?" You ship a new "you may also like" widget on the product template. Three weeks later a developer notices conversion is flat. Was it the widget? Was it something else?
If you've been auditing the catalog weekly and storing scores, you can answer that in one query. From the historical tracking guide:
r = requests.get(
f"{BASE}/history/domains",
headers={"X-API-Key": API_KEY},
)
domains = r.json()["domains"]
print([d for d in domains if d["trend_30d"] < -3.0])
Plus per-URL deltas on each weekly run. A theme update that knocks 6 points off the product template shows up immediately as ~all product pages dropping in lock-step, which is a much louder signal than "conversion was flat for a few weeks."
History retention is tied to plan: Starter gets 30 days, Basic 90, Pro a year, Ultra unlimited. For an ecommerce site that does seasonal launches and quarterly theme work, a year of history is the minimum you actually want — that's Pro at $39/mo.
Plan tier vs catalog size
Working numbers we've used to size clients:
| Catalog size | Weekly cadence audits | Monthly total | Tier |
|---|---|---|---|
| 500 products | 500 × 4 = 2,000 | 2,000 | Basic — $15/mo |
| 2,000 products | 2,000 × 4 = 8,000 | 8,000 | Pro — $39/mo |
| 5,000 products | 5,000 × 4 = 20,000 | 20,000 | Ultra — $99/mo |
| 25,000+ | Stratified sample | varies | Ultra with a sampling strategy |
Free and Starter don't fit anywhere on this table because batch isn't available on Free and the monthly cap on Starter (200 audits) barely covers a one-time audit of a 200-product store. Ecommerce starts at Basic.
What this replaces
Three things this weekly workflow replaces, in order of how much manual labor it removes:
- The "let me click through 20 random products and see if anything looks broken" QA pass that nobody on the team has time for and that misses the long tail anyway.
- The PageSpeed Insights one-URL-at-a-time check that you can only realistically do for the homepage and a few hero products.
- The third-party audit tool that requires installing a crawler on your laptop and leaving it running overnight. That's a workflow from 2018. Your ecommerce platform changes weekly; your audit should run weekly too.
Getting started
If you're running a Shopify store or any catalog over 1,000 products, grab a Basic key for the first month, run the weekly job, look at the second week's deltas, and decide if Pro's larger quota and longer history retention pays for itself. For most stores that crossed the 2,000-product mark, it does — the conversation with developers gets much sharper when you can point at "every product page lost 4 SEO points on Tuesday" instead of "things feel off."