Skip to content
Published Authored byBilly Reiner

Schema · How-to

Custom JSON-LD via theme.liquid

Custom JSON-LD on Shopify lives in two scopes, in theme files Shopify's SEO FAQ also points to for titles and headings1. Site-wide entities (Organization, WebSite) go in theme.liquid inside <head>, printed on the home page, which is where Google reads them. Per-template entities (Product, BreadcrumbList, Article) go in the section file for the template — main-product.liquid for PDPs, main-list-collections.liquid for collection pages, main-article.liquid for blog posts. The Liquid block uses dynamic variables (product.title, shop.url, article.published_at) so the markup composes per-page. Deployment is via Theme Editor (Online Store > Themes > Actions > Edit code), Shopify CLI (shopify theme push), or the GitHub theme integration.

The split between theme.liquid and section files matters because of cardinality. Organization describes the brand — one entity for the whole site. Product describes a single product — one entity, one PDP. Putting Product schema in theme.liquid means the product.* Liquid variables are unbound on non-product pages and the schema fails to render. Putting Organization in main-product.liquid means it prints on every product page and never on the home page, which is the page Google recommends for it.

What custom JSON-LD on Shopify means

Custom JSON-LD on Shopify is structured data the merchant adds to the theme above and beyond what Shopify auto-emits. Shopify's SEO overview confirms themes auto-emit Product schema. Custom JSON-LD covers everything else — Organization, WebSite, BreadcrumbList, Article on the blog, FAQPage on the FAQ hub, plus the Product fields the theme's auto-emission leaves out (itemCondition, hasMerchantReturnPolicy, shippingDetails, and gtin on themes that don't print it). The injection pattern is a <script type='application/ld+json'>…</script> block placed in theme.liquid or a per-template section file.

Mental model: Shopify gives you Product for free; everything else is a Liquid block you write once and reuse. The work is one block per entity type, each block parametrised by Liquid variables so it composes per product, per page, per article.

Three placement scopes

JSON-LD on Shopify lives in one of three scopes: (1) theme.liquid <head>, for site-wide entities like Organization and WebSite, printed on the home page where Google reads them. (2) Per-template section files (main-product.liquid, main-list-collections.liquid, main-article.liquid, page.liquid), for entities that need template-specific Liquid context. (3) Schema apps via Script Tag API or app blocks, for merchants who prefer a managed app to writing Liquid by hand.

theme.liquid placement

theme.liquid is the layout file that wraps every storefront page. Its <head> section is the canonical place for site-wide JSON-LD. Open Online Store > Themes > Actions > Edit code > Layout > theme.liquid. Find the closing </head> tag. Add the Organization and WebSite blocks immediately before </head>, inside a home-page condition: Google requires WebSite on the home page and recommends the home page for Organization. Save. On Dawn and Horizon, delete the theme's own Organization (and on Dawn, WebSite) block in sections/header.liquid so the home page carries one of each.

Liquid theme.liquid <head> placement — Organization and WebSite, paired, on the home page
<!-- Site-wide JSON-LD (Organization + WebSite), home page only -->
{%- if request.page_type == 'index' -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "OnlineStore",
      "@id": "{{ shop.url }}#organization",
      "name": {{ shop.name | json }},
      "url": "{{ shop.url }}/"
    },
    {
      "@type": "WebSite",
      "@id": "{{ shop.url }}#website",
      "url": "{{ shop.url }}/",
      "name": {{ shop.name | json }},
      "publisher": { "@id": "{{ shop.url }}#organization" }
    }
  ]
}
</script>
{%- endif -%}

shop.name goes through | json, which adds the quotes and escapes any quotes inside the name5. One @graph array consolidates Organization and WebSite into a single script tag — cleaner than two separate blocks and equally valid per Schema.org. The request.page_type == 'index' condition keeps both on the home page, where Google reads them7; other pages can still point at {{ shop.url }}#organization by @id. The full Organization and WebSite field sets are on the Organization leaf and WebSite leaf respectively.

Per-template section files

Modern Shopify themes (Dawn-derivative and similar) follow Online Store 2.0 architecture: each template (product, collection, article, page) is composed of sections, and the section file for the dominant section carries the template's structured data. Open Online Store > Themes > Actions > Edit code > Sections. Find main-product.liquid (for the PDP Product + BreadcrumbList block), main-list-collections.liquid (for the collection BreadcrumbList block), main-article.liquid (for the Article block). Dawn's main-product.liquid and main-article.liquid already print {{ product | structured_data }} and {{ article | structured_data }}; replace those lines with your block rather than adding a second one. Elsewhere, paste your JSON-LD inside the section, typically near the bottom before any closing tags.

  • main-product.liquid — Product (gtin, hasMerchantReturnPolicy, shippingDetails, AggregateRating) + per-PDP BreadcrumbList.
  • main-list-collections.liquid — Collection BreadcrumbList.
  • main-article.liquid — Article (headline, author, publisher, dateModified) + per-article BreadcrumbList.
  • page.liquid / sections/main-page.liquid — FAQPage on /pages/faq, custom JSON-LD on landing pages.

The @graph pattern for one block per page

A Shopify page can carry multiple JSON-LD blocks (the theme's Product, plus your blocks for BreadcrumbList, Article, etc.), and Google says it understands multiple items whether they're nested or in separate blocks. The cleaner pattern is one script tag per page containing a @graph array of the entities. @id is what connects them: Google's guidelines use @id to link related items, so when your Product's Offer names its seller by the Organization's @id, the reference points at the same node wherever that node is defined. What @id is not documented to do is merge two separate descriptions of one entity, so describe each entity once.

Practical implementation on Shopify: you'll typically have two script tags on the home page — one in theme.liquid (Organization + WebSite as a @graph) and whatever the home page's sections print — and one or two on other templates, from the section file. Both validate. Consolidating into a single page-wide @graph requires moving the section-file content into theme.liquid with template conditionals, which gets messy. Separate blocks are fine, as long as each entity is described in only one of them6.

Deploying via Theme Editor, Shopify CLI, or GitHub

Three deployment paths in 2026. (1) Theme Editor: edit theme.liquid and section files directly in Online Store > Themes > Actions > Edit code. Saves are live immediately on the published theme; merchants should use a draft theme for risky changes. (2) Shopify CLI: shopify theme pull to download a local copy, edit, shopify theme push to deploy. CLI is the modern recommendation. (3) GitHub integration: connect a repo, commit theme changes, Shopify auto-deploys. Best for teams; requires the Online Store Theme version control feature.

Validation workflow

After deploying, validate one URL per template type. PDP: submit one product URL to Rich Results Test. Collection: submit one collection URL. Blog post: submit one article URL. Homepage: submit shop.url. Expected pass-fail per template: PDP detects Product + Offer + BreadcrumbList (zero errors); Collection detects BreadcrumbList; Blog post detects Article + BreadcrumbList; Homepage detects Organization. The test won't show WebSite: site names aren't supported in the Rich Results Test, so check that block in the Schema Markup Validator. For any errors, edit the relevant section file or theme.liquid, redeploy, re-validate.

A subtle gotcha: Shopify caches theme assets aggressively. After a push, the new JSON-LD may not appear in the Rich Results Test for 30–60 seconds. If the validator returns stale results, wait briefly and re-run.

Shopify gotchas on custom JSON-LD

Five gotchas. First: curly (smart) quotes in the JSON. They don't come from Shopify's code editor; they come from drafting or pasting code through a word processor, a document editor or a chat app, which swap straight quotes for curly ones. A curly quote isn't a JSON string delimiter, so the block stops parsing. Copy code from a plain-text source such as a code editor or the raw file. Second: printing Liquid text variables (product.title, shop.name, article.title) inside quote marks instead of through | json. A double quote or line break in the value breaks the JSON, and | escape doesn't help: it is an HTML filter. Third: putting Product schema in theme.liquid (where product.* is unbound off PDPs). Fourth: putting Organization schema in a section file (where it fires per-template instead of site-wide). Fifth: missing the application/ld+json mime type on the script tag — the block is then ignored by validators.

A sixth gotcha for stores using schema apps alongside custom JSON-LD: the schema app injects its own JSON-LD via Script Tag API after page load, which means view-source on a Shopify storefront sometimes shows your hand-rolled block AND the app's block, and Rich Results Test parses both. Either remove the app's emission (most apps offer a toggle) or let the app own Product entirely and only author site-wide blocks in theme.liquid yourself. The trade-off is covered on the schema-app-vs-theme leaf.