Skip to content
Back to all articles
Shopify SEO
Technical SEO
Canonical URLs
Liquid
Storefront Filtering

Shopify Canonical URLs: The Duplicate Content Trap in Collections and Filters

How Shopify canonical URLs actually work, why collection-scoped product URLs and filter parameters multiply your index, and the theme-level fixes that hold.

August 30, 2026 11 min readBy Matheus Abrahão

The same product, at four different addresses

Shopify canonical URLs are the single most misunderstood thing in Shopify technical SEO, and the reason is that the platform mostly gets them right — right up until a theme, an app or a merchandiser gets them wrong, and then nothing tells you.

Here is the shape of the problem. One product, on a stock store, is reachable at least four ways:

  • /products/linen-shirt
  • /collections/shirts/products/linen-shirt
  • /collections/new-arrivals/products/linen-shirt
  • /products/linen-shirt?variant=4409130

Add a collection with filters and it gets worse. Storefront filtering reflects every applied filter in the URL as a query parameter, and Shopify lets a merchant configure up to 25 filters on a collection. Filters combine with AND logic and values within a filter combine with OR, so a collection with a colour filter, a size filter and a price range is a combinatorial URL generator:

/collections/shirts?filter.v.option.color=blue
/collections/shirts?filter.v.option.color=blue,white
/collections/shirts?filter.v.option.size=m&filter.v.option.color=blue
/collections/shirts?filter.p.tag=new&filter.v.availability=1
/collections/shirts?filter.v.price.gte=40&filter.v.price.lte=90
/collections/shirts?sort_by=price-ascending
/collections/shirts?page=3

None of those are new content. All of them are crawlable if something links to them.

What Shopify gives you for free

Shopify exposes a global Liquid object, canonical_url, that holds the canonical URL for the current page. Themes are expected to render it in the head of layout/theme.liquid:

<link rel="canonical" href="{{ canonical_url }}" />

That one line does most of the work. On a stock theme, a product reached through a collection path resolves its canonical back to the bare /products/<handle> address, so the four URLs above consolidate into one.

The trap is that this is a theme responsibility, not a platform guarantee. Nothing on the platform forces the tag to exist, forces it to be unique, or stops a second one from being injected below it.

The three ways it breaks in production

1. The tag is missing on one template. Custom templates, landing-page-builder templates and app-generated pages are the usual suspects, because they often bypass the standard layout. Check the head of every template type you have, not just the product page.

2. There are two canonical tags. An SEO app injects one, the theme renders one, and they disagree. Search engines will pick one or ignore both. This is the most common finding I have on stores that have installed and uninstalled two or three SEO apps over the years — uninstalling an app does not always remove the snippet it added to theme.liquid.

3. Someone hardcoded the host. A canonical pointing at a host that immediately redirects is a canonical pointing at nothing useful. I did this to my own site: sitemap entries and canonicals pointed at the bare domain while the site served on the www host and 301'd everything to it. Every canonical named a URL that redirected. Search Console kept the site down to four indexed URLs until I fixed it. Nothing errored, nothing warned, and it took a rank check across every service page to notice.

Verify it the boring way. Open your product page in incognito, view source, and search for rel="canonical". Count the matches. There should be exactly one, and it should be an absolute URL on the host you actually serve.

Internal linking is what creates the duplicates in the first place

Canonicals are damage control. The real fix is not generating the alternate URLs at scale.

In Liquid, product.url gives you /products/<handle>. The within filter is what produces the collection-scoped variant:

{% comment %} Emits /collections/shirts/products/linen-shirt {% endcomment %}
<a href="{{ product.url | within: collection }}">{{ product.title }}</a>

{% comment %} Emits /products/linen-shirt {% endcomment %} <a href="{{ product.url }}">{{ product.title }}</a> ```

Many themes use within: collection on product cards, because it powers the "next/previous product in this collection" navigation on the product page. That is a genuine UX feature and it has a genuine SEO cost: every collection page becomes a source of collection-scoped links to the same products.

The decision I make on most stores is to keep within only where the collection context is actually used by the template, and to use the bare product.url everywhere else — related products, search results, cart, upsell blocks, email templates, feeds. Then the canonical is a backstop rather than the primary mechanism.

Filters: do not noindex your best pages by reflex

The standard advice is "block filtered URLs." I think that is wrong more often than it is right.

Some filtered URLs are genuinely valuable landing pages. /collections/shirts?filter.v.option.color=white is answering a real query with a real intent. If white shirts are a meaningful part of your business, that URL should probably become a real collection with its own handle, its own title, its own copy and its own place in the sitemap — not a parameter you hide.

The split I use:

  • Promote the two or three filter combinations that map to real demand. Build them as actual collections. Now they have canonical URLs of their own and can rank.
  • Leave alone the ordinary single-value filters. Canonicalised back to the collection, they cost you very little.
  • Block the combinatorial tail and anything with no landing intent — sort orders, multi-value stacks, internal search results.

Internal search is the clearest case. Search result pages have no business in an index, and they are the easiest thing in the world to accidentally expose. You can block them at the crawl layer with the robots.txt.liquid template, adding a rule inside the group that targets all crawlers:

{% for group in robots.default_groups %}
  {{- group.user_agent }}
  {%- for rule in group.rules -%}
    {{ rule }}
  {%- endfor -%}
  {%- if group.user_agent.value == '*' -%}
    {{ 'Disallow: /*?q=*' }}
  {%- endif -%}
  {%- if group.sitemap != blank -%}
    {{ group.sitemap }}
  {%- endif -%}
{% endfor %}

That is Shopify's own documented pattern: loop the default groups so you inherit the rules Shopify maintains, and add yours conditionally. Do not replace the whole file with static text — the default rule set gets updated and you would freeze it.

One thing worth being precise about: Disallow in robots.txt prevents crawling, not indexing. A URL that is blocked from crawling can still appear in results if enough pages link to it, and because the crawler cannot fetch it, it cannot see your canonical or your noindex either. Use robots.txt for crawl budget on worthless URLs. Use canonicals for consolidation. They are different tools and blocking a page you wanted consolidated is a self-inflicted wound.

Removing pages from the index, properly

Shopify gives you two levers that most merchants never find:

  • The `seo.hidden` metafield. Set it to 1 on a page, blog post or product and Shopify removes it from your sitemap, from search engines, and from your online store's own search.
  • Unlisted product status. Sets a product so it stays reachable by direct link but drops out of sitemaps, collection pages and store search.

Both are better than a hand-rolled noindex conditional in theme.liquid, because they also fix the sitemap. A noindex meta tag on a URL that is still listed in your sitemap is a contradiction you are asking a crawler to resolve, and it wastes crawl budget every time.

Hreflang, if you run Markets

If your store uses Shopify Markets, Shopify emits hreflang tags automatically through content_for_header, and those tags stay in sync with each page's canonical URL. That is the important part: the automatic tags and the canonical agree by construction.

Shopify's own warning is worth repeating verbatim in spirit — adding your own hreflang tags on top of the automatic ones produces duplicate or conflicting annotations and can hurt your ranking. If your theme or an app already outputs hreflang, turn one of them off. Two sources is always worse than either source alone.

The audit, in order

This is what I actually run on a new store, and it takes about an hour:

  • View source on one product, one collection, one filtered collection, one page and one blog post. Count canonical tags on each. Exactly one, absolute, on the live host.
  • Grep the theme for within: and decide, per usage, whether the collection context is real.
  • Open /robots.txt and read it. If it has been customised, read the robots.txt.liquid template and check it still loops robots.default_groups rather than hardcoding rules.
  • Pull the URL list from Search Console's Pages report and sort by "Duplicate, Google chose a different canonical" and "Alternate page with proper canonical tag." The first bucket is your bug list. The second is the system working.
  • Check every canonical resolves with a 200, not a 301. This is the one that took my own site out of the index.

Duplicate content on Shopify is rarely a content problem. It is a URL-generation problem with a one-line fix that four different things can quietly undo.


Related services

Canonicals, filters and index hygiene are implementation work, not advice work — see Shopify SEO expert services for what that covers, and Shopify expert development for the theme-level work behind it. If a replatform is what broke your canonicals, Shopify migration expert covers the redirect and URL-inventory side. Related reading: robots.txt and sitemap.xml on Shopify.


I fix Shopify technical SEO on production stores. See [Shopify SEO expert services](/shopify-seo-expert) or [hire a Shopify developer](/hire-shopify-developer).

Direct: [WhatsApp +55 11 98851-2788](https://wa.me/5511988512788) · [contato.matheusabrahao@gmail.com](mailto:contato.matheusabrahao@gmail.com)

Need a senior engineer who thinks like an operator?

I take on a small number of Shopify operations and senior engineering engagements each quarter. If your store needs catalog hygiene, technical SEO, performance, or marketing automation done right — let's talk.

Continue reading

Vamos conversar