| name | shopify-json-ld |
| description | Use when adding structured data / JSON-LD / schema markup to a Shopify store: recipe, FAQ, Article/BlogPosting, CollectionPage, AboutPage, or Organization schema; chasing Google rich-results / rich-snippet eligibility; debugging a Search Console "Invalid object type" or "missing field" schema error; or a request like "add schema to my store", "add recipe schema", or "why isn't my product showing rich results". This skill adds SUPPLEMENTAL JSON-LD the theme cannot emit dynamically, stored in a metafield and rendered by one Liquid snippet. Not for meta titles, meta descriptions, canonical tags, or Open Graph (use shopify-seo-metadata for those). |
| compatibility | Requires Shopify Admin API access (custom-app token or Shopify CLI 3.x) plus theme/Liquid edit access to add one render snippet. GraphQL written against Admin API 2025-07. |
Shopify JSON-LD
Adds supplemental structured data to a Shopify store: the schema types the
theme can't generate on its own (Recipe, FAQPage, richer Article/Collection
nodes), stored as JSON-LD in a per-entity metafield and rendered through a
single Liquid snippet. The governing principle: you add what's missing, you
never duplicate what the theme already emits. For meta tags, descriptions, and
Open Graph (a different job), use the sibling skill shopify-seo-metadata.
Store access
Lane A: custom-app token (scriptable). In Shopify admin: Settings → Apps
and sales channels → Develop apps → create an app → grant this skill's minimum
scopes, then install and copy the Admin API access token. Export it; never write
it to a committed file:
export SHOPIFY_STORE="your-store.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="<your Admin API access token>"
Minimum scopes: read_products, write_products, read_content,
write_content. There is no standalone metafield scope; metafield access is
governed by the owning resource's scope (products → write_products,
pages/articles → write_content). Rendering the snippet needs theme edit
access (theme editor or the theme's Git repo), which is separate from the API.
Sanity-check the token:
curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ shop { name } }"}'
Lane B: Shopify CLI OAuth (no stored token). shopify store auth --store $SHOPIFY_STORE --scopes read_products,write_products,read_content,write_content then
shopify store execute. Good for token-less stores where the owner logs in.
Toolkit preflight. Lane B rides on the Shopify CLI: run shopify version
first and install it if missing. For the full Admin GraphQL schema and
validated execution, pair this skill with Shopify's official AI toolkit
plugin. In Claude Code, check claude plugin list; if it isn't there:
claude plugin marketplace add Shopify/Shopify-AI-Toolkit
claude plugin install shopify-plugin@shopify-ai-toolkit
Recommended, not required: Lane A needs only curl and a token. The toolkit
gives your agent the API; this skill gives it the playbook.
Supplemental-only doctrine: audit before you generate
Modern Shopify themes already emit Product schema on PDPs (price, variants,
availability, and, with a review app like Judge.me, Yotpo, or Stamped,
aggregateRating), plus Organization/WebSite sitewide and often
Article/BreadcrumbList. Duplicating any of it creates conflicting nodes that
degrade eligibility, not improve it.
So the first move is always read-only: fetch the homepage, a PDP, a collection,
and a blog post, and extract every <script type="application/ld+json"> block.
curl -s "https://$SHOPIFY_STORE/products/<some-handle>" \
| grep -o '<script type="application/ld+json">[^<]*' | head
Whatever the theme already covers, you do not touch. Supplemental schema is
a separate type on the same page (a FAQPage or Recipe node alongside the
theme's Product node), never a second Product node. If the theme's Product
schema is broken or missing, fix the theme. Don't paper over it with a
duplicate.
The metafield + snippet pattern
Store the JSON-LD in a metafield and render it verbatim. Nothing transforms the
value at render time, so the stored string must already be valid JSON-LD.
-
Define the metafield (Settings → Custom Data → the entity type → Add
definition): namespace custom, key json, type multi_line_text_field.
Create it for each entity type you'll populate (Products, Collections, Pages,
Articles).
-
Add one render snippet to theme.liquid, before </head>:
{%- if request.page_type == 'product' and product.metafields.custom.json != blank -%}
<script type="application/ld+json">{{ product.metafields.custom.json }}</script>
{%- elsif request.page_type == 'collection' and collection.metafields.custom.json != blank -%}
<script type="application/ld+json">{{ collection.metafields.custom.json }}</script>
{%- elsif request.page_type == 'page' and page.metafields.custom.json != blank -%}
<script type="application/ld+json">{{ page.metafields.custom.json }}</script>
{%- elsif request.page_type == 'article' and article.metafields.custom.json != blank -%}
<script type="application/ld+json">{{ article.metafields.custom.json }}</script>
{%- endif -%}
- Write the value with
metafieldsSet (GraphQL). It upserts by
namespace/key, so it updates an existing metafield instead of duplicating it:
this sidesteps the classic REST trap of POSTing a second metafield when one
already exists. Store the JSON compact (JSON.stringify(obj), no pretty
spacing). Preview the target count before a bulk write; a count far above
expectation means stop and re-scope. Full pipeline and per-entity prompts:
references/pipeline-and-prompts.md.
Two schema rules that are non-negotiable
Google validates every typed node it finds, including deeply nested ones. A
single malformed node fails the whole block. Two rules prevent nearly all of it:
- No bare
Product nodes. Google requires offers, review, or
aggregateRating on every @type: "Product". In supplemental schema you
rarely have those (the theme owns them), so referencing a product by name uses
@type: "Thing", never Product.
- No partial
VideoObject nodes. VideoObject requires name,
description, thumbnailUrl, uploadDate, and a contentUrl or embedUrl.
If you don't have real video metadata, don't emit VideoObject: use Thing
or an ItemList of ListItem entries instead.
These two mistakes are the usual cause of "Invalid object type for field" and
"missing field" errors in Search Console.
FAQPage is not a rich-result win anymore
Google removed FAQ rich results for non-authoritative (commerce) sites in
2026. FAQPage markup on a product or store page will no longer render the
expandable Q&A snippet in search. It's still valid structured data and still
feeds machine understanding of the page, but do not sell or promote it as a
rich-result / SERP-visibility win. That claim is stale. Recipe, and where
genuinely applicable Article, remain eligible; verify current eligibility per
type against Google's docs (see Provenance) before promising a visible result.
Hallucination-guarded generation
When an agent generates FAQ or descriptive schema from product/page content, it
may only assert facts present in the source text. No invented dimensions,
weight limits, materials, or features. If the source description is thin,
generate fewer entries: three grounded Q&As beat five with two fabricated
answers, and a fabricated spec in schema is a factual claim Google (and the
customer) can catch. The prompt must prohibit fabrication explicitly and prefer
"omit" over "guess". Per-entity prompt scaffolding is in the reference.
Verify against the Admin API, not the storefront
After a write, the storefront can serve the old JSON-LD for minutes to hours:
Shopify's CDN caches the rendered page. Confirm the write by reading the
metafield back through the Admin API, not by curling the product page:
curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ product(id:\"gid://shopify/Product/<ID>\") { metafield(namespace:\"custom\",key:\"json\"){ value } } }"}'
The metafield value is ground truth. Once it's correct, the storefront will
catch up on its own cache cycle: that lag is not a bug to chase.
Non-destructive defaults
- Read before you write; a blank metafield cell in a bulk update deletes the
value, it doesn't skip it. Fill-missing-only unless overwrite is intended.
- Back up before a bulk write (export the existing
custom.json values first).
- Never print or commit the Admin API access token; read it from the env.
References
- references/pipeline-and-prompts.md: the
generation pipeline (fetch → filter → generate → validate → write) and
per-entity prompt recipes (Product FAQ, Recipe, Collection, Page, Article),
loaded on demand.
Provenance and maintenance
Last verified: 2026-07-05. GraphQL pinned to Admin API 2025-07; Shopify
deprecates versions quarterly, so confirm the version before trusting a
version-specific claim. Google's rich-result eligibility changes: FAQ rich
results were deprecated for commerce sites, and other types shift too, so
re-verify per schema type against
Google's structured-data docs
before promising a visible SERP result. Canonical type definitions live at
schema.org. Read-only re-verification a stranger can run:
curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ shop { name } }"}'
curl -s "https://$SHOPIFY_STORE/products/<some-handle>" \
| grep -c 'application/ld+json'
Then validate any live URL in Google's Rich Results Test
(https://search.google.com/test/rich-results). These skills capture operational
lessons the docs don't; they don't replace the schema.org or Google references.