Most Shopify SEO issues aren’t platform flaws. They come from how the theme links to products, which tag and filter pages get indexed, and how apps stack duplicate markup on top of each other. Shopify gets canonicals, sitemaps and hosting right by default, so the work is cleaning up the layer built on top of it.
What Shopify handles well out of the box
Before fixing anything, know what you don’t need to touch:
- Canonical tags on every template, output by
{{ canonical_url }}intheme.liquid. Nearly every theme includes it; confirm yours does. - An XML sitemap at /sitemap.xml, generated and updated automatically, with separate child sitemaps for products, collections, pages and blog posts.
- A default robots.txt that already blocks cart, checkout, account, internal search and sorted collection URLs.
- Automatic redirects when you change a product or collection handle, as long as the “create a URL redirect” box stays checked.
- HTTPS, a global CDN and editable titles, meta descriptions, handles and image alt text on every resource.
So the work starts with the theme and app layer, where those defaults get undone. For platform-neutral crawl and indexing checks, use the technical SEO audit checklist; this post covers what’s specific to Shopify.
Duplicate product URLs and canonicals
Every product lives at /products/handle. It’s also reachable at /collections/any-collection/products/handle for every collection it belongs to, and at ?variant= URLs for each variant. A product in six collections with four variants resolves at dozens of addresses.
Shopify points the canonical on all of them to /products/handle, so this isn’t a penalty. The problem is internal linking. Many themes, older ones especially, build product card links with the within filter:
<a href="{{ product.url | within: collection }}">
Every collection page then links to the collection-aware URL, so your internal links point at pages that canonicalize somewhere else. Google usually follows the canonical, but it has to reconcile conflicting signals across thousands of links, and crawling goes to URLs you never want indexed.
The fix
- Search the theme code for
within: collection, usually in product card, product grid and collection snippets. - Replace it with plain
{{ product.url }}. - Check related products, quick-view modules and breadcrumbs. Apps and custom sections often build their own links.
- Crawl the site afterward and confirm internal links to
/collections/*/products/*have dropped to zero or close to it.
You lose path-based breadcrumbs on themes that read the collection from the URL. That’s a fair trade: a breadcrumb built from a primary collection stored in a product metafield works fine.
Where duplicates come from
| URL pattern | Created by | Risk | What to do |
|---|---|---|---|
/collections/x/products/handle | Theme links using within: collection | Split internal link signals, wasted crawl | Link to product.url everywhere |
/products/handle?variant=ID | Variant pickers, ads, feeds | Low; canonicals to the product | Usually nothing |
/collections/x/tag-name | Tag filtering | Thin near-duplicates of the collection | Promote to a real collection or noindex |
/collections/x?filter.… | Storefront filters | URL combinations multiply fast | Keep out of the index |
/collections/vendors?q=…, /collections/types?q=… | Auto-generated vendor and type pages | Thin pages with no copy | Noindex or block unless used as landing pages |
Variants need attention only when colorways have their own search demand. Then split them into separate products with unique copy and cross-link them; splitting without new copy just creates near-duplicates.
Collection pages, tags and filters
Collections are usually a store’s best category-level ranking pages, and most are underbuilt: a product grid, a generic title and no copy. Give each important collection a unique SEO title, a meta description written for clicks, and a short block of copy on fit, materials or how to choose, typically 100 to 300 words that don’t push the grid below the fold.
Tag pages
Every tag on a collection creates a URL like /collections/shirts/linen. That page shows a subset of products under the same title and meta description as the parent (some themes append the tag name) with no unique copy. Twenty collections with fifty tags each can mean a thousand thin URLs.
The decision rule: does the tag combination match a real query with search demand? If “linen shirts” does, build a proper automated collection at /collections/linen-shirts with a tag condition, its own title and copy, and a place in navigation. Everything else gets noindexed. Add this inside the <head> in theme.liquid:
{%- if current_tags -%}
<meta name="robots" content="noindex, follow">
{%- endif -%}
That covers blog tag pages too, since current_tags is set on both.
Filters
Storefront filters generate parameter URLs such as ?filter.v.option.color=Black and ?filter.v.price.gte=50. Open yourstore.com/robots.txt to see what’s already blocked: the default rules cover sort orders and, on most stores, multi-filter combinations, but single-filter URLs typically stay crawlable. Filtered URLs rarely deserve to rank. If “black leather boots” has demand, make it a collection. Filters are for shoppers; collections are for search.
Keep paginated collection pages crawlable and indexable; noindexing them cuts off links to products deep in large collections. If your theme uses infinite scroll or a “load more” button, make sure real ?page= links still exist in the HTML.
robots.txt.liquid and crawl control
Shopify lets you customize robots.txt by adding a robots.txt.liquid template in the theme code editor. The template renders Shopify’s default rules through the robots.default_groups object. Keep that loop and append your own rules to it:
{% for group in robots.default_groups %}
{{- group.user_agent }}
{%- for rule in group.rules -%}
{{ rule }}
{%- endfor -%}
{%- if group.user_agent.value == '*' -%}
{{ 'Disallow: /collections/vendors*' }}
{{ 'Disallow: /collections/types*' }}
{{ 'Disallow: /collections/*filter.v.price*' }}
{%- endif -%}
{%- if group.sitemap != blank -%}
{{ group.sitemap }}
{%- endif -%}
{% endfor %}
Replacing the loop with static text drops the default rules Shopify maintains. Rules I hold to:
- robots.txt controls crawling, not indexing. A blocked URL that’s already indexed can stay indexed, and Google can’t see a noindex tag it isn’t allowed to fetch. Noindex first, wait until the pages drop out, then block if crawl waste still matters.
- Never block
/collections/,/products/or/cdn/wholesale. Blocking asset paths can break how Google renders your pages. - Check the live file after publishing and review the robots.txt report in Search Console.
- Expect small gains on small catalogs. Crawl rules matter most when tags and filters multiply a few hundred products into thousands of URLs.
For removing a single page from search, Shopify’s seo.hidden metafield (set to 1) takes it out of the sitemap and adds a noindex tag, with no theme edits.
Structured data in themes and apps
Most modern themes output Product JSON-LD on product pages, often through the structured_data Liquid filter. Then a review app adds its own Product markup with ratings, an SEO app adds another, and code from an uninstalled app still sits in a snippet. The result is several Product entities on one page with different prices or availability, and ratings attached to only one. Google may pick the wrong one or drop your review stars. The fix:
- Run a product page through Google’s Rich Results Test and the Schema Markup Validator. Count the Product entities.
- Choose one source for Product markup. Many review apps can write ratings to Shopify’s standard product rating metafields, which the theme can read into its own JSON-LD. Alternatively, let one app own all product markup.
- Turn off schema output in every other app, and search the theme for leftover snippets from apps you’ve removed.
- Confirm price, currency and availability in the markup match the visible page and your Merchant Center feed.
- Add BreadcrumbList markup if the theme lacks it, and Article markup on blog posts.
Content and blog limitations
Shopify’s content system is its weakest area for brands that rely on organic content:
- Fixed URL prefixes. Products, collections, pages and blog posts always sit under
/products/,/collections/,/pages/and/blogs/blog-name/. No nested hierarchies like/learn/topic/subtopic/. - Tags instead of categories. Blog tags create thin
/blogs/news/tagged/…pages; the noindex snippet above handles them. If you need categories, create separate blogs, such as/blogs/how-to/and/blogs/recipes/. - A basic editor. Tables, jump links, FAQ blocks and product embeds need custom sections or HTML. Build article templates with sections and metafields so writers get these without touching code.
- No author pages. Posts are attributed to staff accounts with no native bio page. Store author bios in a metaobject and render them on articles if expertise matters in your category.
Two things work well. Metaobject entries can publish as their own web pages, which suits glossaries or ingredient libraries. And content links easily to commerce: put product blocks inside articles and link relevant articles from collections.
If you’re moving from another platform, URL changes are the main SEO risk. Shopify redirects only fire when the old path returns a 404, so a live page at the old URL blocks its own redirect. The WooCommerce to Shopify migration checklist covers redirect mapping.
Shopify SEO checklist
-
canonical_urlis output intheme.liquidon every template - No
within: collectionlinks left; collection-path product links near zero in a crawl - Tag pages with search demand rebuilt as real collections; the rest noindexed
- Filter URLs kept out of the index; the live robots.txt reviewed
-
robots.txt.liquidkeeps therobots.default_groupsloop and only appends rules - Nothing blocked in robots.txt that still needs a noindex to be seen
- Paginated collection pages crawlable, with real
?page=links - Every important collection has a unique title, meta description and copy
- One Product schema source per page, with ratings, price and availability consistent
- Leftover code from uninstalled apps removed from the theme
- Blog tags noindexed; categories handled with separate blogs if needed
- Redirects mapped for every changed handle, and none shadowed by live pages
- Sitemap submitted in Search Console and Bing Webmaster Tools
Most of this lives in theme code, not settings, which is why I handle it as part of Shopify development work: fix the templates once and every new product and collection inherits it.
Get it built
If your store has collection-path links, thin tag pages or schema conflicts from app sprawl, I’ll fix them in the theme instead of adding another app on top. Start with a Growth Audit, $1,500 fixed and credited if we continue, or scope a Shopify build or migration from $6,000. See pricing or get in touch.