🛍️

GEO for Shopify: Complete Implementation Guide

beginner

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

  1. View source on any product page
  2. Search for "@type": "Product"
  3. 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:

  1. Online Store → Themes → Edit code
  2. Create templates/robots.txt.liquid
  3. 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 */}
  </>
)