Hand-Authored Per-Market JSON-LD for the Bundles & Kits Collection
One Liquid snippet in the layout's head carries a complete schema.org graph for the Bundles & Kits collection — breadcrumb, collection page, a product list with offers, and the page's FAQ — written twice, once for the Canadian storefront and once for the US one.
A Shopify theme emits no entity graph for a collection page — no ItemList, no Offer per listed product, no FAQPage. On Three Ships we hand-authored one for the Bundles & Kits collection, in two versions, one per storefront domain, each a single @graph of BreadcrumbList, CollectionPage, an ItemList of Product-with-Offer nodes and a six-question FAQPage, cross-referenced by @id to the Organization node the layout already emits.
The Problem
Shopify doesn't emit structured data; themes do, and a theme's collection template usually emits none. Palo Alto 5.8.0 marks up the organization in the layout and the product on the product page, and leaves the collection page as it is: a grid of products a crawler can see and a machine can't read as a list. There's no ItemList, no Offer for any product in the grid, no FAQPage behind the accordion at the bottom of the page.
That gap matters most on exactly this kind of page. A bundle collection is the page an answer engine reaches for when someone asks which set suits dry skin or whether a routine saves money over buying the pieces. If the page carries nothing machine-readable, the answer is assembled from whatever the engine can scrape from the grid — or from another site.
The Constraint
Three Ships runs two storefronts on two domains, Canadian and US, in two currencies. Every entity that appears in a collection graph — the canonical URL of the page, the URL of each product, the priceCurrency of each offer, the country each offer serves, the language of the page — differs between them. A single graph written for one domain would be wrong on the other, and a Liquid loop over collection.products can't state the things a hand-written graph can: which products are Routines, Duos or Kits; which country an offer is for; the six questions and answers the page's FAQ accordion shows.
So the graph is hand-authored rather than looped, and because it's hand-authored, each storefront needs a version of its own. The snippet also had to fit the head the theme already builds: the layout emits an Organization node of its own, and the collection graph had to refer to that node rather than declare a second one.
What We Built
bundles-kits-collection-schema.liquid carries two graphs, one written for the Canadian storefront and one for the US storefront, and emits only the one for the domain serving the request. Each is a single <script type="application/ld+json"> with one @graph of four nodes.
A BreadcrumbList — Home, Shop All, Bundles & Kits — with an @id on the collection URL. A CollectionPage with its own name and description, inLanguage set to en-CA or en-US, a breadcrumb reference to the list above, a mainEntity reference to the product list below, and a publisher reference to /#organization on the storefront's own domain. An ItemList whose every ListItem wraps a Product node: name, the product's URL on that domain, a Brand of Three Ships, a category of Routines, Duos or Kits, an image, and an Offer carrying priceCurrency in that storefront's currency, price, availability, the offer's url, and an areaServed Country of Canada or United States. And a FAQPage of six Question/Answer pairs — how much a bundle saves, whether a shopper can build their own, which bundle suits firming, which suits dry or dehydrated skin, which suits a beginner, how to choose — the same six the collection page's accordion section displays.
The nodes reference each other by @id rather than nesting, and the most important reference leaves the file. theme.liquid emits the site's Organization node with an @id built from request.origin plus /#organization. The collection graph's publisher names that same identifier, so a consumer merging the page's graphs finds one organization, defined once in the layout, referenced from the collection page.
@id from the request origin; the collection graph names that same identifier as its publisher, so the page resolves to one organization defined once. Reconstructed from the code — no record of the design decision survives.The wiring is in the layout. theme.liquid renders the snippet in <head> behind request.page_type == 'collection' and collection.handle == 'bundles-kits', and the snippet checks the handle again on its own before emitting anything.
Why This Way
Referencing by @id instead of nesting keeps one graph per page and lets the collection page point at an organization it doesn't define. Nesting an Organization inside the CollectionPage would have put a second, competing definition of the brand on the page next to the layout's; a reference makes the layout's node the only one. The same principle holds inside the graph — the breadcrumb and the item list are declared once and referenced, not repeated inside the page node.
Hand-authoring is what makes the graph say more than the catalog does. The category values, the areaServed country on each offer, the page's name and description written for the market, and the six answers are things no Shopify object holds; a loop over collection.products could name the products and stop there.
Why Not an App
An SEO app emits what it can infer from Shopify objects. On a collection page that's a CollectionPage, sometimes a BreadcrumbList, occasionally a product list built from the grid. It has no way to know which products are Routines and which are Duos, which country an offer is for, or which six questions a shopper actually asks about bundles — none of that is a Shopify field. The app's output is generic by construction, which is fine for the long tail of collections and not fine for the one collection an answer engine is asked about most. What the graph names instead comes from the merchandising taxonomy the collection already uses and from the questions its own accordion answers — both authored, neither derivable.
Implementation Notes
- Two graphs in one snippet file, roughly 1,100 lines between them, each a complete
@graphfor one storefront: the Canadian storefront withCAD,en-CAandareaServedCanada; the US storefront withUSD,en-USandareaServedUnited States. - Every
@idis an absolute URL on the storefront's own domain with a fragment —#breadcrumb,#collectionpage,#itemlist,#faq— so the four nodes are addressable and theCollectionPagereferences them by identifier. - The
publisherreference is to/#organizationon the same domain, which matches the@idthe layout builds fromrequest.originfor itsOrganizationnode. areaServedis set perOfferas aCountry, so the market is stated on the offer itself rather than inferred from the currency.- Each
Productcarries acategoryof Routines, Duos or Kits — a merchandising taxonomy the collection has and Shopify's product model doesn't. - The six
Questionnames are the six headings in the collection page's accordion section, so theFAQPagedescribes content a shopper can see on the page. - The snippet is rendered from the layout's
<head>, not from the collection template, so it's present regardless of which sections the template mounts.
Edge Cases
- Any other collection: the layout's gate never renders the snippet, and the snippet's own handle check emits nothing even if it were rendered from somewhere else.
- A product page for one of the bundles: the gate is on
request.page_type == 'collection', so the graph appears only on the collection, and the product page keeps its own product markup. - On either storefront: the layout's
Organization@idand the graph'spublisherreference are the same identifier, so the page resolves to one organization. - A consumer that flattens the graph without following
@ids still finds every node fully declared, because each is a top-level member of@graphrather than a bare reference.
Platform Primitives Used
- Structured data (JSON-LD) — the graph itself: four node types,
@id-referenced, one@graphper storefront. - Liquid snippets — the graph lives in a snippet rendered from the layout, gated on request type and collection handle.
Where It Runs
One collection on both storefront domains. The snippet is rendered from theme.liquid's head whenever the request is the Bundles & Kits collection page, so it applies to whichever collection template that collection uses and to no other page. Both versions were confirmed emitting on the live storefronts, each with its own currency, language and organization reference.
What This Demonstrates
- Fixed bundles and kit merchandising — the primary capability: the collection where bundles are merchandised is the one that carries a machine-readable list of them, with a category per set and the questions shoppers ask.
- Structured data for AI answers and rich results — a hand-authored entity graph forked per market so each storefront's collection resolves to its own entities.
- International SEO and hreflang — the same graph in two market-specific versions, each with its own canonical URLs, currency and language.
How We Know
One snippet file, roughly 1,100 lines, read from the theme, plus the layout that renders it and the layout's own Organization node. Both graphs were confirmed present on the live storefronts, with the expected currency, language and organization identifier in each. Documented from the code; no client-side record of the decision survives. The structure is verifiable; the reasoning above is our reading of it.
Related Builds
- Product-page FAQ accordion: page-backed answers, mobile 'View More' reveal, auto-emitted FAQPage — same storefront: a
FAQPagegenerated from the accordion's own content rather than authored beside it. - Eight WebMCP tools that make a product page answerable — and buyable — by a browser agent — same storefront: the product page made addressable to agents, where this collection is made legible to them.
- Product card with award badges, per-variant content payload and per-card structured data — Nudestix: product markup emitted per rendered card, the generated counterpart to this authored list.
- Metafield-driven two-level FAQ accordion grafted into a help-center app's shell — LUS Brands: FAQ content structured in a metafield so the theme can both render and mark it up.
The Buy-vs-Build Question
An SEO app buys generic collection markup kept current without attention; a hand-authored graph buys categories, countries and answers no Shopify field holds, at the cost of editing it by hand. Where that line falls is argued at structured data and schema markup: buy or build?.
Provenance & Evidence
- Client: Three Ships — threeshipsbeauty.ca and threeshipsbeauty.com
- Surface: Collection listing
- Templates served: whichever collection template the Bundles & Kits collection uses, on both domains
- Complexity: Medium — one snippet, roughly 1,100 lines across two graphs
- Attribution: Deploi-authored. The snippet and both graphs are ours. It's rendered by the layout of Palo Alto 5.8.0 by Presidio Creative, a paid premium theme the client licenses, and its
publisherreference points at theOrganizationnode that layout emits. - Status: Live, verified 2026-09-07
- Evidence: the snippet and the layout, read from the theme; both graphs confirmed on the live storefronts
- Confidence: Moderate — documented from the code; no client-side record of the decision survives
- Not claimed: any rich-result outcome, any citation by an answer engine, any figure for what a bundle saves
- Primary capability: Fixed bundles and kit merchandising
Ready to Give Your Best Collection a Graph a Machine Can Read?
If the collection you'd most like an answer engine to quote is still a grid and nothing else, the markup is a snippet away — and the harder question is which fields are worth writing by hand. Contact us today and we'll go through your collections with you.