GEO for Shopify: Complete Implementation Guide
beginnerPublished April 18, 2026 · Updated May 13, 2026
Shopify's Dawn v15+ generates Product, Organization, and BreadcrumbList schema automatically. Warning: installing a SEO app that also generates schema creates duplicate structured data — verify with Google Rich Results Test before adding any schema app. For Hydrogen (headless), implement JSON-LD manually in Server Components.
GEO for Shopify: Complete Implementation Guide
Shopify implements GEO via theme.liquid for meta tags and JSON-LD schema. Use Liquid’s built-in article.published_at and article.updated_at for recency signals. Add Product and Article schema via script tags in the layout. Shopify’s CDN-delivered HTML is fully crawlable by AI bots.
Shopify generates server-rendered HTML for all pages — products, collections, blog posts, and static pages. This makes it natively compatible with AI crawlers. The main GEO work is adding structured data and ensuring meta tags are complete.
theme.liquid Head Section
Edit layout/theme.liquid to add complete meta tags:
{% comment %} layout/theme.liquid {% endcomment %}
<!DOCTYPE html>
<html lang="{{ shop.locale }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- TITLE AND DESCRIPTION -->
<title>
{%- if template == 'index' -%}
{{ shop.name }} — {{ shop.description }}
{%- elsif template == 'product' -%}
{{ product.title }} | {{ shop.name }}
{%- elsif template == 'article' -%}
{{ article.title }} | {{ blog.title }} | {{ shop.name }}
{%- elsif page_title -%}
{{ page_title }} | {{ shop.name }}
{%- else -%}
{{ shop.name }}
{%- endif -%}
</title>
{%- if page_description -%}
<meta name="description" content="{{ page_description | escape }}">
{%- else -%}
<meta name="description" content="{{ shop.description | escape }}">
{%- endif -%}
<meta name="author" content="{{ shop.name }}">
<link rel="canonical" href="{{ canonical_url }}">
<meta name="robots" content="index, follow">
<!-- OPEN GRAPH -->
{%- if template == 'article' -%}
<meta property="og:type" content="article">
<meta property="article:published_time" content="{{ article.published_at | date: '%Y-%m-%dT%H:%M:%SZ' }}">
<meta property="article:modified_time" content="{{ article.updated_at | date: '%Y-%m-%dT%H:%M:%SZ' }}">
<meta property="article:author" content="{{ article.author }}">
{%- for tag in article.tags -%}
<meta property="article:tag" content="{{ tag }}">
{%- endfor -%}
{%- elsif template == 'product' -%}
<meta property="og:type" content="product">
{%- else -%}
<meta property="og:type" content="website">
{%- endif -%}
<meta property="og:title" content="{{ page_title | escape }}">
<meta property="og:description" content="{{ page_description | default: shop.description | escape }}">
<meta property="og:url" content="{{ canonical_url }}">
<meta property="og:site_name" content="{{ shop.name }}">
<meta property="og:locale" content="{{ shop.locale | replace: '-', '_' }}">
{%- if template == 'product' and product.featured_image -%}
<meta property="og:image" content="{{ product.featured_image | img_url: '1200x630' }}">
{%- elsif template == 'article' and article.image -%}
<meta property="og:image" content="{{ article.image | img_url: '1200x630' }}">
{%- endif -%}
<!-- JSON-LD SCHEMA -->
{%- if template == 'article' -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": {{ article.title | json }},
"description": {{ article.excerpt_or_content | strip_html | truncate: 160 | json }},
"author": {
"@type": "Person",
"name": {{ article.author | json }}
},
"publisher": {
"@type": "Organization",
"name": {{ shop.name | json }},
"logo": {
"@type": "ImageObject",
"url": {{ shop.url | append: '/assets/logo.png' | json }}
}
},
"datePublished": {{ article.published_at | date: '%Y-%m-%dT%H:%M:%SZ' | json }},
"dateModified": {{ article.updated_at | date: '%Y-%m-%dT%H:%M:%SZ' | json }},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": {{ canonical_url | json }}
}
{%- if article.image -%}
, "image": {{ article.image | img_url: '1200x630' | json }}
{%- endif -%}
}
</script>
{%- endif -%}
{%- if template == 'product' -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 300 | json }},
"url": {{ canonical_url | json }},
"sku": {{ product.selected_or_first_available_variant.sku | json }},
"brand": {
"@type": "Brand",
"name": {{ product.vendor | json }}
},
"offers": {
"@type": "Offer",
"price": {{ product.selected_or_first_available_variant.price | money_without_currency }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "{% if product.available %}https://schema.org/InStock{% else %}https://schema.org/OutOfStock{% endif %}",
"url": {{ canonical_url | json }}
}
{%- if product.featured_image -%}
, "image": {{ product.featured_image | img_url: '1200x1200' | json }}
{%- endif -%}
}
</script>
{%- endif -%}
{{ content_for_header }}
</head>
robots.txt in Shopify
Shopify generates robots.txt automatically. To customize it, go to Online Store → Themes → Edit code and create templates/robots.txt.liquid:
{% comment %} templates/robots.txt.liquid {% endcomment %}
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Claude-User
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: BingBot
Allow: /
{% for group in robots.default_groups %}
User-agent: {{ group.user_agent }}
{% for rule in group.rules %}{{ rule }}
{% endfor %}
{% endfor %}
Sitemap: {{ routes.root_url }}sitemap.xml
Sitemap: {{ routes.root_url }}llms.txt
llms.txt via Shopify Page
Create a new page with handle llms-txt and configure a route:
In config/routes.json:
{
"/llms.txt": "page?handle=llms-txt"
}
Create a template templates/page.llms-txt.liquid:
{% layout none %}
HTTP/1.1 200 OK
Content-Type: text/plain
# {{ shop.name }}
> {{ shop.description }}
## Products
{% for product in collections.all.products limit: 20 %}
- [{{ product.title }}]({{ shop.url }}{{ product.url }}): {{ product.description | strip_html | truncate: 100 }}
{% endfor %}
## Blog
{% for article in blogs.news.articles limit: 20 %}
- [{{ article.title }}]({{ shop.url }}{{ article.url }}): {{ article.excerpt | strip_html | truncate: 100 }}
{% endfor %}
## Pages
- [About Us]({{ shop.url }}/pages/about): About our company
- [Contact]({{ shop.url }}/pages/contact): Contact information
FAQPage Schema for Blog Articles
For blog posts that answer questions, add FAQPage schema:
{% if article.metafields.custom.faq_questions %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{% for question in article.metafields.custom.faq_questions %}
{
"@type": "Question",
"name": {{ question.q | json }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ question.a | json }}
}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
</script>
{% endif %}
GEO Checklist for Shopify
- theme.liquid: complete meta tags with og:type, og:title, og:description, og:url, og:image
- Article pages: article:published_time and article:modified_time via article.published_at and article.updated_at
- Article pages: JSON-LD Article schema in head
- Product pages: JSON-LD Product schema with pricing and availability
- Canonical URL: canonical_url Liquid variable in head
- robots.txt.liquid: all 8 AI crawlers explicitly allowed
- llms.txt: created as Shopify page with custom template
- Blog post content: inverted pyramid (answer in first paragraph)
- Product descriptions: direct value statement in first sentence
- Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1
Dawn v15+: Native Schema Markup
Shopify’s official Dawn theme (v15.0 and later) generates Product, Organization, and BreadcrumbList schema automatically.
⚠️ Duplicate Schema Warning: If your theme already generates Product schema and you install a SEO app that also generates schema, AI crawlers receive duplicate, potentially conflicting structured data:
- View source on any product page
- Search for
"@type": "Product" - If more than one schema block appears, you have duplicates — remove one source
AI Crawler robots.txt Configuration
Shopify allows customizing robots.txt via a Liquid template:
- Online Store → Themes → Edit code
- Create
templates/robots.txt.liquid - Add:
{% assign default_robots = shop.metafields.seo.robots | default: '' %}
{{ default_robots }}
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Google-Extended
Allow: /
Shopify Hydrogen: JSON-LD in Server Components
Hydrogen (headless Shopify on Remix) requires manual JSON-LD implementation:
// In your product route component
const jsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.title,
description: product.description,
offers: {
"@type": "Offer",
price: product.priceRange.minVariantPrice.amount,
priceCurrency: product.priceRange.minVariantPrice.currencyCode,
availability: product.availableForSale
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* rest of component */}
</>
)