Shopify Builds>Three Ships>Market-forked product JSON-LD authored in the product section, with a templated tier for the catalog

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.

ONE PRODUCT SECTION · RENDER TIME Hero product page requested product.handle matches an authored block .ca graph — complete @id …threeshipsbeauty.ca/products/…#product Product · Offer · shipping · returns · FAQ · breadcrumb currency and shipping region for that market brand → /#organization on this domain .com graph — complete @id …threeshipsbeauty.com/products/…#product Product · Offer · shipping · returns · FAQ · breadcrumb currency and shipping region for that market brand → /#organization on this domain Layout's Organization node @id {{ request.origin }}/#organization request.host contains '.ca' / '.com' .ca branch .com branch brand reference brand reference no @id crosses
As we read the code: the fork is on the hostname, each branch is a whole document with identifiers on its own market's domain, and each points its `Brand` reference at an organization identifier on that domain. Nothing in a `.ca` graph points into the `.com` one.

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 on localization.country or a market handle, so each authored block is gated to the hostname whose URLs it contains.
  • Nine hero products carry a .ca/.com pair; the best-sellers collection carries a pair keyed on collection.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, #faq on the product URL — and Brand references point at /#organization on that market's domain, which is where the layout publishes its Organization node.
  • Authored blocks take two document shapes, and both are in the file: some emit one <script> per type, others wire Product, offers, shipping, returns and FAQPage — and in several, a BreadcrumbList — into a single @graph. The AggregateOffer form appears in the per-type blocks.
  • The templated tier reads its GTIN properties from the variant barcode, and builds its offers array by iterating product.variants with priceCurrency read from cart.currency.iso_code.
  • name and description in the templated tier pass through strip_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 CollectionPage and ItemList graph 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-JSON payload 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 image property in the templated tier rather than an empty array, and a variant with a blank SKU gets no sku.
  • A product viewed at its plain /products/ URL rather than through the best-sellers collection path has no collection in 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.
  • Brand references in the trial-kit blocks and the templated tier are composed from request.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, CollectionPage and ItemList, wired by @id.
  • Shopify Markets — the two hostnames are the two markets; the templated tier reads the market's currency from cart.currency.iso_code and the authored tier is forked on request.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

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.

More builds

Product-page FAQ accordion: page-backed answers, mobile 'View More' reveal, auto-emitted FAQPage

A local extension of Palo Alto's Tabs section that hides every FAQ row past the third on mobile behind one reveal button, and emits `FAQPage` JSON-LD from the vendor's own block loop, reading each answer from the same Shopify page or rich-text field the accordion displays.

Three Ships

Product card with award badges, per-variant content payload and per-card structured data

One snippet renders the whole Nudestix grid card: tag and award badges, quick view, wishlist, comparison, an in-grid add to bag, and a per-variant data payload the shade switcher reads without a fetch.

Nudestix

Metafield-driven two-level FAQ accordion grafted into a help-center app's shell

The help-center page kept the app's shell. The answers inside it moved into a Shopify page metafield, and a two-level accordion we wrote renders them — deep-linkable, keyboard-operable and reusable elsewhere in the theme.

LUS Brands

GA4 ecommerce dataLayer instrumentation across a Rebuy cart

A client-side event layer that hangs off the network rather than off the DOM: a `fetch` wrapper that recognizes a cart add, a diff against the previous cart so the event reports what changed, and item taxonomy read from a block the product page already renders.

Three Ships

Configurable PDP block system on the Palo Alto theme (Recharge widget placement, size charts, siblings, quick view)

Merchandiser-orderable buy-box blocks on Three Ships' licensed theme, with the subscription app block captured and re-emitted inside the buy box, plus metafield-driven size charts, a product-family switcher and a sticky add-to-cart bar.

Three Ships