Quality, Performance, Accessibility, and Shipping Beginner 10 min read
SEO Basics in Themes
Learn essential SEO practices for Shopify themes including meta tags, structured data, canonical URLs, and content optimization.
Good SEO is built into theme architecture. A well-optimized theme helps merchants rank better in search engines without requiring technical expertise.
SEO Fundamentals
Theme developers control:
- Meta tags (title, description)
- Heading structure
- Structured data (JSON-LD)
- Canonical URLs
- Page speed (covered in performance)
- Mobile-friendliness
Title Tags
Dynamic Titles
{# layout/theme.liquid #}
<title> {%- if page_title == blank -%} {{ shop.name }} {%- elsif page_title contains shop.name -%} {{ page_title }} {%- else -%} {{ page_title }} | {{ shop.name }} {%- endif -%}</title>Template-Specific Titles
{# Product page title #}{%- capture page_title -%} {{ product.title }}{% if product.variants.size > 1 %} - {{ product.selected_or_first_available_variant.title }}{% endif %}{%- endcapture -%}
{# Collection page title #}{%- capture page_title -%} {{ collection.title }} {%- if current_tags %} - {{ current_tags | join: ', ' }}{% endif %} {%- if current_page > 1 %} - Page {{ current_page }}{% endif %}{%- endcapture -%}
{# Search page title #}{%- capture page_title -%} Search results{% if search.terms != blank %} for "{{ search.terms }}"{% endif %}{%- endcapture -%}Meta Descriptions
Description Snippet
{# snippets/meta-description.liquid #}
{%- capture meta_description -%} {%- if template.name == 'product' -%} {{ product.metafields.global.description_tag | default: product.description | strip_html | truncate: 155 }} {%- elsif template.name == 'collection' -%} {{ collection.metafields.global.description_tag | default: collection.description | strip_html | truncate: 155 }} {%- elsif template.name == 'page' -%} {{ page.metafields.global.description_tag | default: page.content | strip_html | truncate: 155 }} {%- elsif template.name == 'article' -%} {{ article.metafields.global.description_tag | default: article.excerpt_or_content | strip_html | truncate: 155 }} {%- elsif template.name == 'blog' -%} {{ blog.metafields.global.description_tag | default: 'Articles from ' | append: blog.title }} {%- else -%} {{ shop.description | default: shop.name }} {%- endif -%}{%- endcapture -%}
<meta name="description" content="{{ meta_description | strip_newlines | escape }}">Canonical URLs
Prevent duplicate content issues:
{# layout/theme.liquid #}
<link rel="canonical" href="{{ canonical_url }}">
{# For paginated pages #}{%- if paginate.previous -%} <link rel="prev" href="{{ paginate.previous.url }}">{%- endif -%}{%- if paginate.next -%} <link rel="next" href="{{ paginate.next.url }}">{%- endif -%}Heading Structure
Proper Hierarchy
{# Product page #}<h1>{{ product.title }}</h1>
<section> <h2>Description</h2> {{ product.description }}</section>
<section> <h2>Reviews</h2> {# Reviews content #}</section>
<section> <h2>Related Products</h2> {# Related products #}</section>Common Mistakes
{# Bad: Multiple H1s #}<h1>{{ shop.name }}</h1> {# Logo #}<h1>{{ product.title }}</h1> {# Product title #}
{# Good: Single H1, logo is link #}<a href="/">{{ shop.name }}</a><h1>{{ product.title }}</h1>Structured Data (JSON-LD)
Organization Schema
{# snippets/schema-organization.liquid #}
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "Organization", "name": {{ shop.name | json }}, "url": {{ shop.url | json }}, {%- if settings.logo -%} "logo": {{ settings.logo | image_url: width: 500 | prepend: 'https:' | json }}, {%- endif -%} "contactPoint": { "@type": "ContactPoint", "email": {{ shop.email | json }}, "contactType": "customer service" }}</script>Product Schema
{# snippets/schema-product.liquid #}
{%- assign current_variant = product.selected_or_first_available_variant -%}
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "Product", "name": {{ product.title | json }}, "description": {{ product.description | strip_html | truncate: 500 | json }}, "url": {{ shop.url | append: product.url | json }}, {%- if product.featured_image -%} "image": {{ product.featured_image | image_url: width: 1200 | prepend: 'https:' | json }}, {%- endif -%} "sku": {{ current_variant.sku | json }}, "brand": { "@type": "Brand", "name": {{ product.vendor | json }} }, "offers": { "@type": "Offer", "url": {{ shop.url | append: product.url | json }}, "priceCurrency": {{ cart.currency.iso_code | json }}, "price": {{ current_variant.price | divided_by: 100.0 | json }}, "availability": "https://schema.org/{% if current_variant.available %}InStock{% else %}OutOfStock{% endif %}", "seller": { "@type": "Organization", "name": {{ shop.name | json }} } } {%- if product.metafields.reviews.rating.value != blank -%} ,"aggregateRating": { "@type": "AggregateRating", "ratingValue": {{ product.metafields.reviews.rating.value.rating | json }}, "reviewCount": {{ product.metafields.reviews.rating_count | json }} } {%- endif -%}}</script>BreadcrumbList Schema
{# snippets/schema-breadcrumbs.liquid #}
{%- if template.name == 'product' -%}<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": {{ shop.url | json }} } {%- if collection -%} ,{ "@type": "ListItem", "position": 2, "name": {{ collection.title | json }}, "item": {{ shop.url | append: collection.url | json }} } ,{ "@type": "ListItem", "position": 3, "name": {{ product.title | json }} } {%- else -%} ,{ "@type": "ListItem", "position": 2, "name": {{ product.title | json }} } {%- endif -%} ]}</script>{%- endif -%}Article Schema
{# snippets/schema-article.liquid #}
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "BlogPosting", "headline": {{ article.title | json }}, "description": {{ article.excerpt_or_content | strip_html | truncate: 200 | json }}, {%- if article.image -%} "image": {{ article.image | image_url: width: 1200 | prepend: 'https:' | json }}, {%- endif -%} "datePublished": {{ article.published_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }}, "dateModified": {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%S%z' | json }}, "author": { "@type": "Person", "name": {{ article.author | json }} }, "publisher": { "@type": "Organization", "name": {{ shop.name | json }} {%- if settings.logo -%} ,"logo": { "@type": "ImageObject", "url": {{ settings.logo | image_url: width: 300 | prepend: 'https:' | json }} } {%- endif -%} }}</script>Open Graph Tags
For social sharing:
{# snippets/social-meta.liquid #}
{# Open Graph #}<meta property="og:site_name" content="{{ shop.name }}"><meta property="og:url" content="{{ canonical_url }}">
{%- if template.name == 'product' -%} <meta property="og:type" content="product"> <meta property="og:title" content="{{ product.title }}"> <meta property="og:description" content="{{ product.description | strip_html | truncate: 200 }}"> {%- if product.featured_image -%} <meta property="og:image" content="{{ product.featured_image | image_url: width: 1200 }}"> <meta property="og:image:width" content="1200"> <meta property="og:image:height" content="{{ 1200 | divided_by: product.featured_image.aspect_ratio | round }}"> {%- endif -%} <meta property="product:price:amount" content="{{ product.price | money_without_currency }}"> <meta property="product:price:currency" content="{{ cart.currency.iso_code }}">{%- elsif template.name == 'article' -%} <meta property="og:type" content="article"> <meta property="og:title" content="{{ article.title }}"> <meta property="og:description" content="{{ article.excerpt_or_content | strip_html | truncate: 200 }}"> {%- if article.image -%} <meta property="og:image" content="{{ article.image | image_url: width: 1200 }}"> {%- endif -%} <meta property="article:published_time" content="{{ article.published_at | date: '%Y-%m-%dT%H:%M:%S%z' }}"> <meta property="article:author" content="{{ article.author }}">{%- else -%} <meta property="og:type" content="website"> <meta property="og:title" content="{{ page_title }}"> <meta property="og:description" content="{{ page_description | default: shop.description }}">{%- endif -%}
{# Twitter Card #}<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="{{ page_title }}"><meta name="twitter:description" content="{{ page_description | default: shop.description | truncate: 200 }}">Image SEO
{# Always include alt text #}{{ product.featured_image | image_url: width: 800 | image_tag: alt: product.featured_image.alt | default: product.title}}
{# Use descriptive filenames (handled by merchant) #}{# Good: blue-cotton-t-shirt.jpg #}{# Bad: IMG_12345.jpg #}URL Structure
Shopify handles URL structure, but themes can:
{# Use handles for clean URLs #}<a href="{{ product.url }}">{{ product.title }}</a>
{# Avoid query parameters when possible #}{# Good: /collections/shirts/products/blue-tee #}{# Avoid: /products/blue-tee?variant=12345 #}Robots.txt and Sitemap
Shopify generates these automatically:
{# Available at /sitemap.xml #}{# Robots.txt at /robots.txt #}
{# Link to sitemap in layout #}<link rel="sitemap" type="application/xml" href="{{ shop.url }}/sitemap.xml">SEO Checklist
## Theme SEO Checklist
### Meta Tags
- [ ] Dynamic title tags per template- [ ] Meta descriptions with fallbacks- [ ] Canonical URLs- [ ] Pagination links (prev/next)
### Structured Data
- [ ] Organization schema- [ ] Product schema with offers- [ ] BreadcrumbList- [ ] Article schema for blog posts
### Social Meta
- [ ] Open Graph tags- [ ] Twitter Card meta- [ ] Share images at correct sizes
### Content
- [ ] Proper heading hierarchy- [ ] Alt text for images- [ ] Semantic HTML structure
### Technical
- [ ] Mobile-friendly design- [ ] Fast page load- [ ] Sitemap linkedTesting SEO
Tools
- Google Rich Results Test
- Schema.org Validator
- Facebook Sharing Debugger
- Twitter Card Validator
- Google Search Console
Practice Exercise
Implement SEO for a theme:
- Add dynamic meta tags
- Implement Product schema
- Add Open Graph tags
- Test with Rich Results Test
- Validate structured data
Key Takeaways
- Dynamic meta tags for each template
- Structured data helps search engines understand content
- One H1 per page in proper hierarchy
- Canonical URLs prevent duplicate content
- Alt text on all meaningful images
- Social meta for sharing
- Test with validation tools
What’s Next?
The next lesson covers Cross-Browser Testing and Shopify Quirks for compatibility.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...