Technical SEO for Ecommerce: The Complete Checklist
Ecommerce technical SEO is harder than content-site SEO because the URL space is dynamic. A 200-product Shopify store can produce 50,000+ crawlable URLs once you account for variants, filters, sort orders, and pagination. Most of those URLs should not be indexed — but the wrong ones blocked, or canonicalized wrongly, will quietly suppress rankings on your money pages. This guide is a working checklist for the technical issues unique to ecommerce: faceted navigation, variant canonicals, product schema, parameter handling, out-of-stock URLs, pagination, LCP on product pages, and platform-specific fixes for Shopify, WooCommerce, Magento, and BigCommerce.
What Makes Ecommerce SEO Technically Different
Five structural issues exist on every ecommerce site and almost no other site type:
Faceted navigation explosion. Every filter (color, size, brand, price range) multiplied by every other filter creates a URL. With 5 attributes and 4 values each, you have 5^4 = 625 filter combinations per category. Across 50 categories, that's 31,250 filter URLs. Without a deliberate strategy, Google crawls and indexes the bulk of them as thin duplicates.
Variant pollution. A single t-shirt SKU with 4 colors and 5 sizes is one product to your customer but up to 20 URL variants if your platform exposes them as ?variant= or /products/shirt-blue-medium. Each variant fragment competes with the parent for the same query.
Out-of-stock churn. Inventory turns over weekly. Every item that goes out of stock is a decision: keep indexed, redirect, or remove. Get this wrong on hundreds of SKUs per month and you bleed organic traffic.
Internal search and infinite scroll. Internal site search creates /search?q= URLs that Google can crawl, and infinite-scroll category pages hide products from Googlebot if not paginated server-side.
Schema density. Ecommerce pages need Product, Offer, AggregateRating, Review, BreadcrumbList — and increasingly merchant-listing fields like shippingDetails and hasMerchantReturnPolicy. Missing any of these disqualifies you from rich results that competitors win.
Canonical Strategy for Variants, Colors, and Sizes
The canonical decision for variants depends on whether the variant has independent search demand. A "red Nike Air Max" has search demand. A "size 9.5 Nike Air Max" does not. The rule:
Color variants: if you have separate landing pages per color with unique imagery and copy, give each a self-referencing canonical and include all in the sitemap. If the color is a dropdown on a single page, canonicalize all ?variant= URLs back to the parent product.
Size variants: always canonicalize to the parent product. Sizes have no independent search demand and individual pages would be near-duplicates.
On Shopify, the default behavior is correct out-of-the-box, but custom themes frequently break it. Here is how to verify the variant canonical in theme.liquid or product.liquid:
{%- comment -%}
Correct Shopify canonical for variant URLs.
Place inside <head> of theme.liquid.
Strips ?variant= and canonicalizes to parent product.
{%- endcomment -%}
{%- if template contains 'product' -%}
<link rel="canonical" href="{{ shop.url }}{{ product.url }}">
{%- else -%}
<link rel="canonical" href="{{ canonical_url }}">
{%- endif -%}
{%- comment -%}
Verify by visiting:
/products/example-shirt?variant=12345678
View source — canonical should be /products/example-shirt
(no query string, no variant ID)
{%- endcomment -%}If you sell the same product across multiple categories (e.g., a sneaker that lives under /collections/running and /collections/mens), your canonical must always point to one URL — typically the bare /products/sneaker-name path without a collection prefix. WooCommerce and Magento both default to category-prefixed product URLs, which causes one product to live at multiple URLs simultaneously. Fix this in WooCommerce by going to Settings → Permalinks → Product permalinks and selecting "Default" (the shop base) instead of "Shop base with category."
Product Schema: The Required JSON-LD
As of 2026, Google requires the Product schema with Offer details for any merchant listing eligibility. Missing schema does not just lose rich results — it removes you from the organic shopping carousel that now appears on most product queries. Here is the full schema block to render on every product page:
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Acme Trail Runner v3",
"image": [
"https://example.com/img/trail-runner-1.jpg",
"https://example.com/img/trail-runner-2.jpg"
],
"description": "Lightweight trail running shoe with Vibram outsole.",
"sku": "TR-V3-BLK-9",
"mpn": "ACME-TR-V3-BLK-9",
"brand": { "@type": "Brand", "name": "Acme" },
"offers": {
"@type": "Offer",
"url": "https://example.com/products/trail-runner-v3",
"priceCurrency": "USD",
"price": "129.00",
"priceValidUntil": "2026-12-31",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0",
"currency": "USD"
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "US"
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "US",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"reviewCount": "284"
},
"review": [{
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5"
},
"author": { "@type": "Person", "name": "Sarah K." },
"reviewBody": "Best trail shoe I've owned in years."
}]
}
</script>Two failure modes to watch for: AggregateRating with reviewCount of 0 (Google ignores the schema and may flag a structured data error), and review markup that doesn't match visible on-page reviews (manual-action territory). Only emit AggregateRating when actual reviews exist on the rendered HTML.
Faceted Navigation and Parameter Handling
Faceted navigation is the single largest source of crawl-budget waste on ecommerce sites. The decision matrix for handling each parameter type:
UTM parameters (utm_source, utm_medium): canonical to the clean URL. Never block in robots.txt — Google needs to crawl them to apply the canonical.
Sort parameters (?sort=price-asc, ?sort=newest): canonical to the unsorted parent URL. No SEO value to indexing sorted versions.
Page-size parameters (?per_page=48): canonical to the default-page-size URL.
Single-attribute filters with search demand (?color=black on shoes): self-referencing canonical, include in sitemap, ensure a unique H1 and meta description per filter combination.
Multi-attribute filter mashups (?color=black&size=10&width=wide): canonical to the most general filter that has demand (just ?color=black), or to the parent category if no filter has demand.
Internal search (?q=): block in robots.txt. Internal search results are low-quality, fluctuate constantly, and waste crawl budget.
Here is a robots.txt configuration covering the common ecommerce parameter patterns:
# robots.txt for ecommerce User-agent: * # Block internal search and cart-related URLs Disallow: /search? Disallow: /*?q= Disallow: /cart Disallow: /checkout Disallow: /account # Block infinite-loop filter parameter combinations # Only after confirming via GSC > Crawl Stats that # Googlebot is wasting budget on these Disallow: /*?*color=*&*size=*&*width= Disallow: /*?price_min= Disallow: /*?price_max= # Allow UTM and tracking parameters through (canonicals # handle them) — DO NOT add Disallow: /*?utm_* # Sitemap reference (200s only) Sitemap: https://example.com/sitemap.xml
The decision matrix in plain language: use canonical when the URL serves a real user but should not rank independently (sorts, UTMs, page sizes); use noindex (with follow) when the URL has unique content that you affirmatively don't want indexed (rare on ecommerce); use robots.txt only when you have confirmed crawl-budget waste in GSC and the URLs have no inbound links to consolidate. Avoid noindex on faceted URLs — Google's long-term behavior on noindex+follow is to demote it to noindex+nofollow, which strips internal link equity from your category pages.
Out-of-Stock Products: Don't 404
The single most damaging mistake on ecommerce sites is auto-deleting or 404ing out-of-stock products. Every out-of-stock URL you remove takes its accumulated backlinks, ranking signals, and indexed equity with it. The correct policy depends on whether the product is coming back:
Temporarily out of stock (will restock). Keep the URL live with a 200 status. Update the Product schema availability field to https://schema.org/OutOfStock or BackOrder. Add visible on-page messaging ("Back in stock soon — notify me") with an email-capture form. Do not noindex. Do not redirect.
Permanently discontinued, with a replacement. 301 redirect the old URL to the replacement product. Only do this when the replacement is a true equivalent — not just "something we also sell." A redirect from a discontinued running shoe to your homepage is a soft 404 and Google will treat it as one.
Permanently discontinued, no replacement. Return 410 Gone (not 404). 410 signals deliberate removal and is processed faster by Google. Optionally redirect to the parent category page if backlinks justify it; otherwise let the URL die cleanly.
The schema-level signal for out-of-stock products:
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Acme Trail Runner v3",
"offers": {
"@type": "Offer",
"url": "https://example.com/products/trail-runner-v3",
"priceCurrency": "USD",
"price": "129.00",
"availability": "https://schema.org/OutOfStock",
"itemCondition": "https://schema.org/NewCondition"
}
}For sustained out-of-stock periods (3+ months), add cross-links to similar products in stock — this prevents the page becoming a dead-end for crawlers and gives users a path forward.
Pagination: rel=next/prev Is Dead, What Works Now
Google deprecated rel="next" and rel="prev" in 2019 — but the deprecation only means Google ignores those tags, not that pagination has stopped mattering. The current correct approach:
Self-canonicalize each page. /collections/shoes?page=2 canonicals to itself. /collections/shoes?page=3 canonicals to itself. This keeps every product on those pages crawlable and indexable. Do not canonical pagination back to page 1.
Include all pagination URLs in your sitemap if they contain unique products. Excluding them tells Google those products don't matter.
Server-render pagination links as actual <a href> elements, not buttons that trigger client-side state changes. Googlebot follows <a href> reliably; it does not always follow JavaScript click handlers.
Infinite scroll patterns: if the URL doesn't change as users scroll, also implement a parallel paginated URL structure (?page=2, ?page=3) that Googlebot can crawl. The user gets infinite scroll; the crawler gets crawlable pages. This is sometimes called "progressive enhancement pagination" and is the only pattern that works for both.
Title tags on paginated pages should be differentiated: "Running Shoes - Page 2 - Acme". Identical titles across pagination cause Google to consolidate them and you lose page 2+ from the index.
Hreflang for International Stores
If you sell into multiple countries with localized currency, language, or shipping, hreflang is mandatory. Without it, Google routes UK users to your US site, US users to your CA site, and bounce rates spike across the board. The annotations must be reciprocal — every locale page must declare every other locale, including itself, plus an x-default fallback.
<!-- Place in <head> of every locale variant of the same product --> <link rel="alternate" hreflang="en-us" href="https://example.com/products/trail-runner-v3" /> <link rel="alternate" hreflang="en-gb" href="https://example.co.uk/products/trail-runner-v3" /> <link rel="alternate" hreflang="en-ca" href="https://example.ca/products/trail-runner-v3" /> <link rel="alternate" hreflang="en-au" href="https://example.com.au/products/trail-runner-v3" /> <link rel="alternate" hreflang="de-de" href="https://example.de/produkte/trail-runner-v3" /> <link rel="alternate" hreflang="fr-fr" href="https://example.fr/produits/trail-runner-v3" /> <link rel="alternate" hreflang="x-default" href="https://example.com/products/trail-runner-v3" />
Three failure patterns to audit for: missing reciprocal annotations (the UK page references the US page but the US page does not reference the UK page), hreflang pointing to redirected URLs (the target must return 200, not 301), and hreflang on canonical-target pages only (every locale must self-canonicalize, not canonicalize to a single "master" locale).
For Shopify Markets, hreflang generation is automatic but limited to subfolder configurations. For domain-level internationalization (separate ccTLDs), you must inject hreflang manually via theme.liquid using the alternate URL list from your headless config. WooCommerce requires WPML or Polylang for hreflang output. Magento 2 generates hreflang automatically from store-view configuration if "Add Store Code to URLs" is enabled.
Internal Linking: Category to Product
The internal link graph from category pages to product pages is the most underused ranking lever on ecommerce sites. Every product page should be reachable in 3 clicks or fewer from the homepage, and the deeper a product sits in the hierarchy, the more it depends on internal linking for crawl priority.
Bidirectional category linking. Each category page should link to its parent category and to sibling categories with anchor text containing the category keyword. "Running shoes" on a sneakers category page is far stronger than "view more."
Product-to-product linking. "Customers also viewed" and "Frequently bought together" modules pass link equity between products and signal product clusters. Render these server-side, not via post-load JavaScript, or Google won't see them.
Breadcrumb navigation. Mandatory on every product and category page, with BreadcrumbList JSON-LD. Breadcrumbs replace your URL display in the SERP and they pass internal link equity up the hierarchy.
Footer category links. Limited to 30–50 of your most important categories. Sitewide footer links are heavily devalued by Google but still help discovery for deep pages.
Use Screaming Frog or SitemapFixer's internal-link analysis to identify products with fewer than 3 internal links. These are crawl-priority orphans and almost always rank below their potential.
Site Speed and LCP for Product Pages
Largest Contentful Paint (LCP) on product pages is almost always the hero product image. Anything above 2.5 seconds on mobile fails Core Web Vitals and degrades rankings. The fix is mechanical:
Preload the hero image. Add <link rel="preload" as="image" href="..." fetchpriority="high"> in the <head>. This tells the browser to fetch the LCP image before parsing the rest of the page.
Serve responsive images with srcset and sizes. Mobile devices should download a 600px image, not the desktop 2000px hero. On Shopify, use image_url: width: 600 filters. On WooCommerce, ensure the woocommerce_single_product_image_thumbnail_size filter is configured.
Use modern image formats. AVIF or WebP cuts image weight 30–50% versus JPEG with no perceptible quality loss. All major platforms now generate these automatically — the issue is usually a CDN that strips the format negotiation.
Lazy-load below-the-fold images, not the hero. A common mistake: applying loading="lazy" to the hero image. This delays LCP by hundreds of milliseconds. The hero should be eager-loaded; everything below the fold should be lazy.
Defer third-party scripts. Reviews widgets, chat widgets, and analytics tags routinely block render. Audit with Chrome DevTools Performance panel — anything contributing to render-blocking CSS or JavaScript on the critical path needs async or defer.
Platform-Specific Notes
Shopify. Strong defaults — automatic canonicals on variants, automatic sitemap generation, automatic hreflang with Shopify Markets. Weakness: cannot edit robots.txt without Shopify Plus or the recent robots.txt.liquid editing feature; cannot fully control URL structure (forced /products/, /collections/ prefixes); pagination is fixed at 50 per page on collection pages without theme customization. Audit theme.liquid for hard-coded canonical or schema overrides — third-party themes regularly break Shopify's defaults.
WooCommerce. Maximum flexibility, minimum defaults. Out of the box: no canonical handling for filtered URLs, no Product schema unless you install Yoast SEO Premium or Rank Math, no automatic hreflang. Required plugins for a baseline: an SEO plugin (Yoast/Rank Math), an image optimization plugin (ShortPixel/Imagify), and for international stores WPML or Polylang. Watch for the ?orderby=, ?filter_, and ?attribute_ parameter family — all need explicit canonical rules.
Magento 2. Most powerful for large catalogs but most complex. Native support for canonical tags (Stores → Configuration → Catalog → Search Engine Optimization → Use Canonical Link Meta Tag For Categories/Products = Yes), native hreflang via store views, native Product schema. Common issues: layered navigation generates parameter explosions; the "Search Engine Friendly URLs" setting must be enabled or you get ?id= URLs everywhere; the default sitemap excludes filtered category URLs which is usually correct.
BigCommerce. Mid-complexity, mid-flexibility. Canonical tags handled automatically, sitemap auto-generated, schema injected by Stencil themes by default. Weakness: faceted navigation (BigCommerce Product Filtering) generates URLs that are not canonicalized to the parent — you must manually add canonical rules via theme template edits or use the API to suppress filter URLs from the sitemap.
Monitoring with GSC Performance Segmentation
Most ecommerce sites monitor SEO at the site level — total clicks, total impressions — and miss the segment-level signal that actually drives revenue. Set up these GSC views:
Product pages only. Search Console → Performance → Pages → Filter by Page contains /products/ (or /product/, depending on platform). Track clicks and impressions on this segment over time. A drop here that doesn't show in site-wide numbers is usually a Product schema regression or a price-feed issue.
Category pages only. Filter by Page contains /collections/ or /categories/. Category page traffic correlates with mid-funnel queries ("running shoes" vs "Acme Trail Runner v3"). Drops here usually indicate a faceted-navigation canonical regression diluting the parent.
Indexing → Pages report, segmented. Export the "Crawled - currently not indexed" and "Discovered - currently not indexed" lists. If 70%+ are filtered URLs (containing ?), your faceted nav is wasting crawl budget. If they're mostly product URLs, you have a product-discovery problem — likely missing internal links or a sitemap omission.
Core Web Vitals report, segmented by URL pattern. Track LCP, INP, and CLS separately for product pages, category pages, and the homepage. A failing "mobile" status at the URL-pattern level is the only Core Web Vitals signal that affects ranking — site-level "passing" can hide failures on the templates that matter most.
Related Guides
- Shopify Sitemap: Configuration, Limits, and Common Issues
- Canonical Tags and Duplicate Content: A Practical Guide
- Faceted Navigation SEO: Crawl Budget Without the Mess
- Out-of-Stock SEO: Keep Indexed, Don't 404
- Product Schema: Required Fields and Common Errors
- Ecommerce SEO Checklist 2026: 50+ Items for Online Stores