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>
{# 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 linked

Testing 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:

  1. Add dynamic meta tags
  2. Implement Product schema
  3. Add Open Graph tags
  4. Test with Rich Results Test
  5. Validate structured data

Key Takeaways

  1. Dynamic meta tags for each template
  2. Structured data helps search engines understand content
  3. One H1 per page in proper hierarchy
  4. Canonical URLs prevent duplicate content
  5. Alt text on all meaningful images
  6. Social meta for sharing
  7. 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...