Skip to main content
AI GuidesShopify-native tactics

Adding JSON-LD to Shopify themes safely

JSON-LD is the standard structured-data surface Shopify themes need for AI retrieval. But shipping it naively double-emits schema with Dawn, breaks on special characters in product titles, and leaves invalid markup that retrievers silently skip. Here is the defensive implementation.

Samir Bhattacharya with Hiren Bhuva

Shopify GEO Engineer

9 min
schema-stack.svg
Adding JSON-LD to Shopify themes safelyProduct"@type": "Product"validOffer"@type": "Offer"validReview"aggregateRating": 4.8validFAQPage"@type": "FAQPage"validBreadcrumbList"itemListElement": […]valid

Why JSON-LD belongs in theme Liquid, not in an app

Theme-level JSON-LD is authored once, lives with the page, and cannot be blocked. App-injected schema depends on JavaScript execution and sometimes gets skipped by retrievers.

Shopify merchants have two main options for emitting JSON-LD: write it directly into theme Liquid, or have a Shopify app inject it at render or via client-side JavaScript. Theme-level is almost always the right answer, for three reasons. Theme-emitted JSON-LD is present in the server-rendered HTML that every retriever sees on the first fetch — no JavaScript dependency, no injection timing issues. It is also auditable: you can read exactly what ships by inspecting the .liquid files. And it is resilient: the app can uninstall, the subscription can lapse, the integration can break silently, but the theme snippet keeps working until someone explicitly removes it.

App-injected JSON-LD has its place — mostly when the data source is an external system the app integrates with — but even then the app typically writes Liquid rather than injecting client-side, because the retrieval reliability story is so much better for server-rendered schema. If you are evaluating an app that promises 'AI-ready schema' and the mechanism is client-side JavaScript injection, that is a red flag worth asking about.

19%

of Shopify stores in our audit panel have client-side-only JSON-LD that retrievers can skip

Surfient crawl audit, 847 Shopify stores, Q1 2026. The 19% emit JSON-LD only via JavaScript after page load, meaning retrievers that do not execute JS (which is most of them) never see the structured data.

layer-stack.svgInfographic
The indexing stack from retrievers down to Shopify source data — every layer needs to line up for a citation to land.INDEXING STACKAI RetrieversGPTBot · ClaudeBot · PerplexityBotLAYER 1Context Surfacellms.txt · llms-full.txtLAYER 2Feed Surfaceai-sitemap.xml · products.ndjsonLAYER 3Page SurfaceProduct JSON-LD · FAQPage · HowToLAYER 4Shopify SourceProducts · Metafields · CollectionsLAYER 5FLOW
Figure · layer stackThe indexing stack from retrievers down to Shopify source data — every layer needs to line up for a citation to land.

What Dawn and OS 2.0 themes emit by default — and when to override

Dawn ships some Product, Organization, and WebSite schema automatically. You need to know what so you do not double-emit.

Dawn — Shopify's reference OS 2.0 theme and the basis for most modern Shopify themes — ships JSON-LD automatically on several page types. Any schema you add should check what is already there, because two Product JSON-LD blocks on the same page confuse retrievers and cause validators to pick whichever one they find first (which might be the less-complete default).

Product pages (Dawn)
Dawn emits a basic Product schema with name, description, image, sku, offers (price + availability), and brand. It does NOT emit aggregateRating, review, additionalProperty, or custom fields you might want.
Collection pages (Dawn)
Dawn does not emit CollectionPage schema by default. Opportunity to add it without overlap.
Article pages (Dawn)
Dawn emits basic Article schema with headline, image, datePublished, and author. Often under-populated.
Home / store pages (Dawn)
Dawn emits WebSite schema (with SearchAction) and Organization schema. Both are usually fine; review them before duplicating.
Breadcrumbs
Dawn does NOT emit BreadcrumbList by default. High-ROI add.
FAQ blocks
Dawn does NOT emit FAQPage schema. You must add it if you want FAQ schema.

To see exactly what your current theme ships, use Google's Rich Results Test on a live product page, a live collection page, and your homepage. It will list every JSON-LD block it finds, with field-level parsing — so you know before you add anything whether you are filling a gap or colliding with an existing block.

The safe Liquid pattern for Product JSON-LD

Put schema in a dedicated snippet. Use the | json filter on every value. Render conditionally and test with a regression check.

File: snippets/schema-product.liquid

{%- if product -%}
  {%- assign first_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: 300 | json }},
    "url": "{{ shop.url }}{{ product.url }}",
    "sku": {{ first_variant.sku | json }},
    "image": [
      {%- for img in product.images limit: 4 -%}
        {{ img | image_url: width: 1200 | prepend: "https:" | json }}{%- unless forloop.last -%},{%- endunless -%}
      {%- endfor -%}
    ],
    "brand": {
      "@type": "Brand",
      "name": {{ product.vendor | json }}
    },
    "offers": {
      "@type": "Offer",
      "price": {{ first_variant.price | money_without_currency | json }},
      "priceCurrency": {{ cart.currency.iso_code | json }},
      "availability": {%- if first_variant.available -%}"https://schema.org/InStock"{%- else -%}"https://schema.org/OutOfStock"{%- endif -%},
      "url": "{{ shop.url }}{{ product.url }}"
    }
  }
  </script>
{%- endif -%}

Include it from sections/main-product.liquid

{%- comment -%} Delete Dawn's default Product schema first, then: {%- endcomment -%}
{%- render "schema-product" -%}

Adding BreadcrumbList and FAQPage the same way

Each schema type lives in its own snippet. Include them from the appropriate section or template.

File: snippets/schema-breadcrumb.liquid

{%- assign breadcrumb_collection = collection | default: product.collections.first -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}{{ routes.root_url }}"
    }
    {%- if breadcrumb_collection -%}
    ,
    {
      "@type": "ListItem",
      "position": 2,
      "name": {{ breadcrumb_collection.title | json }},
      "item": "{{ shop.url }}{{ breadcrumb_collection.url }}"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": {{ product.title | json }}
    }
    {%- endif -%}
  ]
}
</script>

File: snippets/schema-faq.liquid

{%- assign faq = product.metafields.ai.faq.value -%}
{%- if faq and faq.size > 0 -%}
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {%- for item in faq -%}
      {
        "@type": "Question",
        "name": {{ item.question | json }},
        "acceptedAnswer": {
          "@type": "Answer",
          "text": {{ item.answer | json }}
        }
      }{%- unless forloop.last -%},{%- endunless -%}
      {%- endfor -%}
    ]
  }
  </script>
{%- endif -%}

Centralising schema includes in layout/theme.liquid

Render all schema snippets in one place with template-type conditionals. Makes audit and change straightforward.

{%- comment -%} In layout/theme.liquid, just before </head> {%- endcomment -%}
{%- render "schema-organization" -%}
{%- if template contains "product" -%}
  {%- render "schema-product" -%}
  {%- render "schema-breadcrumb" -%}
  {%- render "schema-faq" -%}
{%- elsif template contains "collection" -%}
  {%- render "schema-collection" -%}
  {%- render "schema-breadcrumb" -%}
{%- elsif template contains "article" -%}
  {%- render "schema-article" -%}
  {%- render "schema-breadcrumb" -%}
{%- elsif template == "index" -%}
  {%- render "schema-website" -%}
{%- endif -%}
  • One place controls every schema emission — easy to audit, easy to change.
  • Each snippet is self-contained and handles its own guard clauses (no-op if the data it needs is missing).
  • Adding new schema types is a two-line change (new snippet + new render line).
  • Removing a schema type from a template type is a one-line delete.
  • Rich Results Test and schema validators can test each template type independently because the emission is predictable.

Validation and regression testing — catching silent breakage

Theme edits break JSON-LD silently if you do not test. Rich Results Test plus an in-repo regression check catches both classes.

Silent breakage is the real risk with theme-level JSON-LD. A developer or agency edits main-product.liquid six months from now to add a new feature, accidentally wraps the product title in double-quotes instead of using the json filter, and every product page now ships invalid JSON. The theme looks fine, analytics look fine, and AI citations quietly decline for weeks until someone notices.

Google Rich Results Test
Run on one product, one collection, one article, and the homepage after every theme ship. Manual but fast. Catches gross errors.
Schema.org validator
Stricter than Google's. Catches type errors and missing required fields. Worth running on major changes.
In-repo Playwright regression test
A test that loads each template type, parses every JSON-LD block, and asserts shape and required fields. Runs on every commit. The only reliable protection against silent breakage.
Live-site continuous monitoring
A service that crawls your live site periodically and flags schema regressions. Catches the breakage that slipped past the regression test (cache issues, CDN quirks, theme-app conflicts).
The value of schema is that retrievers use it to build confidence. The cost of broken schema is that retrievers use it to lower confidence. You want monitoring that catches broken markup the day it breaks, not the quarter after.
Samir Bhattacharya, Shopify GEO Engineer, Surfient

Frequently asked questions

6

Pulled from the questions merchants ask us most often in advisory calls. Crawlers see these as FAQPage schema — the answers here match what appears in AI citations.

  • Replace it if you want richer markup — Dawn's default Product schema is minimal and missing fields like aggregateRating, review, additionalProperty, and custom spec data that retrievers value. Delete the default block from sections/main-product.liquid and ship your own snippet that includes everything Dawn does plus the additional fields. Extending in place works too but makes the schema harder to audit when you have it scattered across multiple files.

Free · 5 minutes · no signup

Ready to see your store's GEO score?

Run a free Surfient audit and see exactly what ChatGPT, Perplexity, Claude, Gemini, and Google AI Overviews are missing about your store — signal family by signal family.

0

GEO score

Engine readiness

0

Technical indexing

0

Content fit

0

Live example — your number is ready in about 90 seconds.

Keep reading

Browse all AI Guides