Market-Forked Product JSON-LD Authored in the Product Section, With a Templated Tier for the Catalog
Two tiers of structured data at the foot of one Three Ships product section: complete, per-market @graph documents for the hero SKUs, branched on request.host, and a Liquid-templated Product and Offer block that reads variants, currency and barcode for the rest of the catalog.
One Shopify theme serving two market hostnames cannot emit one correct product graph, because price, currency, shipping region and return window differ by host. Inside Three Ships' product section we branch on request.host and emit a complete @graph per market for the hero SKUs — Product, Offer, shipping, returns, FAQ — with every @id on that market's hostname, while a Liquid-templated Product block reads variants, currency and barcode for the rest of the catalog.
The Problem
A Shopify theme's product markup describes one product at one price in one currency. That is what the product object gives it. What it does not give it is the shipping rate and transit window for the market the shopper is in, the return policy that applies there, the questions buyers ask about this product, or the routine it belongs to — none of which are fields on a product, and all of which are what a rich result or an AI answer wants to see spelled out.
Three Ships wanted its hero products described that completely, per product and per market. It sells the same catalog on a .ca and a .com hostname with different currencies, shipping and return windows, so "the product graph" is really two graphs, and the wrong one is worse than none: a .com visitor whose page asserts a Canadian price and a Canadian shipping region is being told something false by markup that reads as authoritative. The answer copy, meanwhile, lived in marketing documents rather than in Shopify fields, so nothing could be templated off the catalog.
The Constraint
The theme is Palo Alto 5.8.0 by Presidio Creative, a paid premium theme, and the product section is the vendor's, already extended by us for the buy box and its blocks. The structured-data layer had to live inside that section without disturbing what the section renders — which is why it sits at the foot of the file, after the markup and the sticky bar.
Two hostnames, one theme. Shopify Markets gives Liquid localization.country and a market-aware cart.currency, but the thing that distinguishes a .ca request from a .com request at render time is the host, and the graph's own identifiers had to be absolute URLs on the right one. So the discriminator is request.host.
The section is mounted by 133 product templates, and only a dozen or so products were worth a complete graph. The rest still needed a valid Product node — one that reads the catalog rather than repeating it — so the layer needed two tiers, not one.
And the answer copy was marketing content rather than data. FAQ answers, routine steps and product facts existed as prose in documents. They could be placed in the graph as authored nodes or not at all.
What We Built
Roughly 5,000 lines of Liquid and JSON-LD at the foot of product.liquid, in two tiers.
The authored tier is a run of paired conditionals — {% if product.handle == '…' and request.host contains '.ca' %} and the matching '.com' block — for nine hero products, plus a pair keyed on collection.handle == 'best-sellers' that fires when a product is reached through that collection's path. The trial kit is the exception that proves the pattern: it exists as two products, one per market, so its two blocks key on the handle alone. Each block emits a complete document for that market, in one of two shapes. Some are single @graph documents: a Product with its Brand, a set of PropertyValue facts and an offers array of Offer nodes carrying OfferShippingDetails with a DefinedRegion, handling and transit windows and a cutoff time, plus a MerchantReturnPolicy; then a FAQPage of Question and Answer pairs; and in several of them a BreadcrumbList up front, with one product's pair also carrying a HowTo and the brand's Organization node inline. The rest emit the same entities as separate <script type="application/ld+json"> documents, one per type, and it is those that carry an AggregateOffer wrapping one Offer per size where a product sells in more than one. The trial-kit documents add a HowTo of HowToSection and HowToStep nodes and a VideoObject beside the Product. The collection pair emits a BreadcrumbList, a CollectionPage, an ItemList of ListItem entries and its own FAQPage.
Every @id in an authored block is absolute on that market's domain — …/products/<handle>#product, #breadcrumb, #faq — and the Brand reference points at a /#organization identifier on that same domain, which is where the layout's site-wide Organization node is published. The templated tier and the trial kit's Product blocks compose that reference from {{ request.origin }} directly, so it is the string the layout emits on whichever hostname served the page. A .ca graph and a .com graph share no identifier.
The templated tier is one Product block driven off Liquid for the rest of the catalog. name and description go through strip_html | json; url is shop.url with the product's relative URL appended; the featured image is rendered at its native width through img_url with https: prepended, and omitted when the product has no featured media. sku comes from product.selected_or_first_available_variant, and the GTIN properties are read from the variant barcode. offers is a loop over product.variants, each an Offer with the variant's sku, its price divided into major units, priceCurrency from cart.currency.iso_code and its own URL. Its Brand reference is composed from request.origin, so it joins the same organization node as the authored tier.
The section also renders three schema snippets around the product markup — breadcrumb, video and how-to graphs — and an enriched product snippet for a single hero SKU, and the same host-fork pattern repeats in a set of per-collection schema snippets for the category pages. Each of those is documented as its own build. A separate application/json payload with the id product-metafield-JSON sits beside the structured data and carries GA4-shaped item fields for the analytics layer; it is not schema.org markup and is described on that page.
Why This Way
Two tiers because the catalog has two kinds of product. A dozen hero SKUs carry the commercial argument and deserve a graph that says everything a machine could want — shipping, returns, questions, routine — in the market's own terms. Everything else needs a valid Product node read from the catalog and nothing more, and the templated tier reads that from the catalog at render time, so it follows the catalog without anyone touching it.
The fork on request.host rather than on a market or country object is a decision about what the page can be sure of. The host is what was requested; it is what the shopper typed and what the crawler fetched, and the identifiers inside the graph have to be URLs on that host. Branching on it makes each market's document self-consistent by construction, and gives the two hostnames two entity graphs that never reference each other.
Two things are paid for that. The editorial half — answers, routine steps, product facts — lives in a theme file, so changing an answer is a theme deploy rather than an admin edit; we chose exact copy in the graph over a content model that did not yet exist, and that is a real cost for the people who own the copy. And because the layer sits inside the product section, it renders in the body after the product markup and ships only where that section is mounted, rather than from the layout's head; it belongs to the product page, which is right, but it means the page's most complete markup is also its last.
Why Not an App
Schema apps generate a Product graph from the product object for every product in the store. That is the right tool when the fields you need are the fields Shopify has. The fields that made these pages worth marking up — a per-market shipping and returns block, a buyer FAQ, a routine with steps, a curated best-sellers list — are not in the product object, so nothing reading only the catalog can emit them. Doing it in the theme also keeps another script off a product page that already carries several, and lets the templated tier serve the long tail from the same file with no second system to configure.
Implementation Notes
- The market split is on
request.host contains '.ca'and'.com', not onlocalization.countryor a market handle, so each authored block is gated to the hostname whose URLs it contains. - Nine hero products carry a
.ca/.compair; the best-sellers collection carries a pair keyed oncollection.handle; the trial kit's two blocks key on handle alone because each market sells its own trial-kit product. - Identifiers follow one scheme per market —
#product,#breadcrumb,#faqon the product URL — andBrandreferences point at/#organizationon that market's domain, which is where the layout publishes itsOrganizationnode. - Authored blocks take two document shapes, and both are in the file: some emit one
<script>per type, others wireProduct, offers, shipping, returns andFAQPage— and in several, aBreadcrumbList— into a single@graph. TheAggregateOfferform appears in the per-type blocks. - The templated tier reads its GTIN properties from the variant barcode, and builds its
offersarray by iteratingproduct.variantswithpriceCurrencyread fromcart.currency.iso_code. nameanddescriptionin the templated tier pass throughstrip_html | json, so rich-text descriptions with quotes or markup cannot break the document.- The best-sellers collection block lives in the product section, so it emits the
CollectionPageandItemListgraph on product pages reached through the collection's path — the URL shape Shopify uses when a shopper clicks through from a collection. - A sibling
product-metafield-JSONpayload in the same section exposes GA4 item fields; the analytics layer fetches product pages and reads it, which is documented on its own page.
Edge Cases
- A product with no featured media gets no
imageproperty in the templated tier rather than an empty array, and a variant with a blank SKU gets nosku. - A product viewed at its plain
/products/URL rather than through the best-sellers collection path has nocollectionin scope, so the collection graph does not render on that view. - The two trial-kit products each carry a full authored document without a host test, because the market split for that product is done by the catalog rather than by the theme.
Brandreferences in the trial-kit blocks and the templated tier are composed fromrequest.origin, so they follow whichever hostname served the page.
Platform Primitives Used
- Structured data (JSON-LD) — the vocabulary itself:
Product,Offer,AggregateOffer,OfferShippingDetails,MerchantReturnPolicy,BreadcrumbList,FAQPage,HowTo,VideoObject,CollectionPageandItemList, wired by@id. - Shopify Markets — the two hostnames are the two markets; the templated tier reads the market's currency from
cart.currency.iso_codeand the authored tier is forked onrequest.host. - Theme sections — the whole layer lives at the foot of the product section, so it renders wherever that section is mounted and nowhere else.
Where It Runs
The product detail page. The product section is mounted by 133 product templates, so every one of them carries the branching, and which tier renders depends on the product and the host. The collection graph renders on product pages reached through the best-sellers collection path.
What This Demonstrates
- Structured data for AI answers and rich results — a per-product, per-market entity graph authored for machines that answer questions, with a templated tier so the long tail follows the catalog.
- Multi-market storefront architecture — one theme, two hostnames, two graphs, forked at render time on the host that was requested.
- International SEO and hreflang — structured data forked per storefront domain, so each market's product page resolves to its own entities.
How We Know
One section file read from the theme — the product section, of which roughly 5,000 lines are this layer — plus the layout file that publishes the organization node the graphs reference, and the product templates that mount the section. Documented from the code; no client-side record of the decision survives. The figure above is our reading of the branching, and it carries the same grade as the prose.
What this page describes is the Liquid, the branching and the shape of the graph. The answer and routine copy inside the FAQPage and HowTo nodes is editorial content rather than anything read from the catalog, and we make no claim about ranking, rich-result or citation outcomes for any of it.
Related Builds
- Product-page FAQ accordion: page-backed answers, mobile 'View More' reveal, auto-emitted FAQPage — Three Ships. The generated pattern on the same storefront: a
FAQPageemitted from the block loop that renders the visible accordion, so the shopper and the retriever get the same words. - Product card with award badges, per-variant content payload and per-card structured data — Nudestix. Structured data emitted per card on the grid, on a Dawn base where the job is not colliding with the theme's own Product node.
- Metafield-driven two-level FAQ accordion grafted into a help-center app's shell — LUS Brands. A
FAQPagegenerated from a metafield corpus, the alternative to authoring answers in a theme file.
The Buy-vs-Build Question
A schema app is the right answer for generic Product and Breadcrumb markup across a whole catalog, and the wrong one when the fields that matter are per-market and editorial. This build is both halves at once: a templated tier that an app could have written, and an authored tier that no app could. Our reasoning on where the line falls is in structured data and schema markup: buy or build?
Provenance & Evidence
- Client: Three Ships — threeshipsbeauty.ca and threeshipsbeauty.com
- Surface: Product detail page
- Templates served: 133 product templates mount the section that carries this layer
- Complexity: High — roughly 5,000 lines of Liquid and JSON-LD
- Attribution: Deploi-authored. The structured-data layer at the foot of the product section is ours. The section it sits in belongs to Palo Alto 5.8.0 by Presidio Creative, a paid premium theme the brand licenses, and is extended by us elsewhere; the site-wide organization node the graphs reference is emitted from the layout and documented as its own build.
- Status: Live, verified 2026-09-07
- Evidence: One section file, one layout file and the product templates, read from the theme; no client working record
- Confidence: Moderate — documented from the code; no client-side record of the decision survives
- Primary capability: Structured data for AI answers and rich results
Ready for Product Pages That Say the Same Thing in Both Markets?
If your .ca and .com storefronts share one theme and one product graph, one of them is asserting the other's prices. Contact us today and we'll show you what a per-market entity graph looks like on the product pages that carry your argument.