Shopify is SEO-capable out of the box — it handles sitemaps, canonical tags, and basic meta tags. The platform’s SEO ceiling is determined by its constraints: URL structure is partially fixed, canonical handling for collection/product paths creates duplicate content issues, and themes vary widely in technical quality. Understanding which constraints are fixed versus addressable determines where to invest optimization effort.
Shopify URL Structure Issues
The /collections/[handle]/products/[handle] duplicate: Shopify generates two valid URLs for every product in a collection:
https://store.com/products/widget(primary product URL)https://store.com/collections/category/products/widget(collection-context URL)
Shopify automatically adds rel="canonical" pointing from the collection URL to the primary /products/ URL — this prevents the duplicate from creating a content penalty. However, the collection URLs still consume crawl budget and may receive internal links that should point to the canonical URL.
Best practice: Link to /products/[handle] in internal links, not the collection-path URL. This concentrates link equity on the canonical page and reduces unnecessary crawl.
URL customization limits: Shopify doesn’t allow fully custom URL structures. Product URLs are /products/[handle], collections are /collections/[handle], pages are /pages/[handle], and blog posts are /blogs/[blog-handle]/[post-handle]. The handles are customizable but the path structure is fixed.
Meta Tags and Title Optimization
Shopify’s default title tag format: [Product Name] - [Store Name]. This is acceptable but not optimal for all products. Override via theme:
In Liquid (Shopify’s templating language), customize title tags in the theme’s head.liquid or equivalent:
{% if template == 'product' %}
<title>{{ product.title }} - Buy {{ product.type }} | {{ shop.name }}</title>
{% elsif template == 'collection' %}
<title>{{ collection.title }} Collection - {{ shop.name }}</title>
{% else %}
<title>{{ page_title }} | {{ shop.name }}</title>
{% endif %}
Meta description: Shopify provides meta description fields in the admin for products, collections, pages, and blog posts. These should be filled for all primary pages — empty meta description fields result in Google generating snippet text from page content, which may not represent the page optimally.
Handling Pagination in Shopify Collections
Large collections paginate: /collections/shoes?page=2. Shopify’s default pagination handling doesn’t implement rel="next"/"prev" (Google deprecated this but it’s still used by some tools). More critically, paginated collection pages may dilute crawl budget.
Optimization: For large collections, implement infinite scroll or “load more” via JavaScript rather than pagination if feasible. For sites where pagination is necessary, ensure paginated pages aren’t indexed (they don’t need to rank independently) but are crawlable for link discovery:
{% if current_page > 1 %}
<meta name="robots" content="noindex, follow">
{% endif %}
App JavaScript and Page Speed
Shopify apps frequently inject JavaScript that impacts page speed. Common offenders:
- Chat widgets loaded synchronously
- Product review apps loading excessive JavaScript
- Loyalty program widgets
- Upsell/cross-sell popup scripts
Diagnosis: Run PageSpeed Insights on key pages before and after disabling apps via Shopify’s admin. Identify which apps contribute most to JavaScript execution time.
Solution approaches:
- Remove unused apps (Shopify stores accumulate installed-but-unused apps that still inject code)
- Use deferred loading: add
deferorasyncattributes to app scripts where the app allows - Limit app code to pages where it’s needed — a reviews app shouldn’t load on checkout pages
Theme selection: Themes built for conversion often sacrifice speed. Before selecting or purchasing a theme, test its PageSpeed scores against real product pages.
Shopify Blog SEO
Shopify’s blog feature supports basic content SEO but has limitations:
- Blog posts live at
/blogs/[blog-name]/[post-handle]— this URL structure is fixed - Categories for blog posts are implemented as “tags” — Shopify creates URLs at
/blogs/[blog]/tagged/[tag]which creates thin tag archive pages
Tag archive issue: Shopify generates indexable pages for every blog tag. With many tags, this creates hundreds of thin archive pages. Options:
- Noindex tag pages: add
<meta name="robots" content="noindex, follow">to tag template - Limit tags to a small, consistent set of meaningful categories
Internal linking from blog: Blog posts should link to relevant product collections and products. Shopify’s blog editor supports HTML, so direct linking to product pages is straightforward.
Shopify SEO Apps
Several Shopify apps extend SEO capabilities:
SEO Manager / Smart SEO: Bulk meta tag editing, JSON-LD schema injection, image alt text management. Useful for stores with many products where manual meta tag optimization is impractical.
Plug In SEO: Automated SEO health checks, structured data, broken link alerts.
Avoiding app dependency: Core SEO elements (title tags, meta descriptions, image alt text, structured data) should be handled via theme code or minimal apps, not heavy third-party applications. Apps that significantly impact page speed trade SEO gains in one area for losses in another.
Structured Data for Shopify
Shopify’s default themes include basic Product schema. Extend it with:
BreadcrumbList: Navigation breadcrumbs with schema improve site architecture signals and enable breadcrumb display in SERP.
Organization: Site-level brand information improves Knowledge Panel eligibility for brand queries.
FAQ (on product pages): Product FAQ sections with FAQ schema can generate rich results with expandable questions directly in SERP.
Inject additional schema via theme’s head.liquid using Liquid to dynamically populate product data:
{% if template == 'product' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 200 | json }},
"sku": {{ product.selected_or_first_available_variant.sku | json }},
"offers": {
"@type": "Offer",
"price": {{ product.selected_or_first_available_variant.price | money_without_currency | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}"
}
}
</script>
{% endif %}