For the complete documentation index, see llms.txt. This page is also available as Markdown.

Product Detail Test — Theme Integration

A Product Image & Title Test compares different titles, descriptions and images for the same product. Unlike every other Eraya test type, this one is rendered by your theme, not by JavaScript — so it needs a one-time integration into your Liquid before your first test can run.

This page is for developers integrating it by hand. You only need to do this once: after the integration is in place, every future product test runs without touching the theme again.

Prefer not to do this yourself? Eraya can patch a duplicate of your theme for you. This guide is for merchants and agencies who want to own the change — for example, to fold it into a theme that's under version control.


Why the theme renders it (and not the SDK)

It's a fair question, because every other Eraya visual test patches the DOM from JavaScript. This one deliberately does not.

A theme renders product.title in far more places than it looks: the <h1>, the breadcrumb, the browser <title>, JSON-LD structured data, the sticky add-to-cart bar, cart line items, search results. If the SDK swapped three of those and missed two, it would have no way to detect it — what a page renders is a fact only Liquid has. The shopper would see a mix that exists in neither variation, and the experiment would measure something nobody designed.

Rendering in Liquid makes that failure impossible by construction. The trade-off is this integration, which is why it exists.

The flow

  1. When a test goes live, Eraya writes a metafield onto each product in the test containing every variation's overrides.

  2. On the visitor's first page, the Eraya SDK buckets them and writes a cart attribute naming their variation, then reloads once.

  3. Every page from then on is server-rendered: your Liquid reads the metafield, reads the cart attribute, and renders that variation.

The control arm gets the cart attribute too. It has to — without it, control orders carry no assignment and can't be attributed, which would silently delete one side of your experiment.


Prerequisites

Requirement
Why

Eraya app embed enabled on the theme

The SDK does the bucketing and writes the cart attribute. No theme.liquid edit is needed — the embed handles it.

A backup of your theme

Duplicate the theme before you start. These are edits to core product rendering.

Shopify Liquid familiarity

You'll be editing main-product, card-product, gallery and cart snippets.

Will this work on my theme?

The contract is theme-agnostic — a product metafield plus a cart attribute, both plain Liquid. Any theme on any Shopify plan can read them, Online Store 2.0 or vintage. Steps 1–3 (resolver, media pool, gallery) work anywhere.

Steps 4 and 5 are shaped by Dawn conventions and need adapting:

Assumption
If your theme differs

A hide_variants section setting strips variant images

Skip that snippet — it exists to stop Dawn's rule from removing images your variation added.

The variant picker reads product.variants | json

Find what your picker actually reads and apply the same rewrite to that. Some themes serialise the whole product, some use a <script type="application/json"> with a custom shape. The rule is unchanged: never leave a media reference pointing at a slide the override removed.

Two of these matter differently:

  • Discovery surfaces — quick view, JSON search, JS-rendered collection cards. These come before the shopper decides, so they're part of what you're testing. Patch them with the JavaScript API.

  • Cart surfaces built from /cart.js. Leave them. The shopper has already seen the variation and acted on it, so the cart is downstream of the decision the experiment measures. More to the point, Shopify's checkout and the order itself always show the real product title — you cannot change that from a theme. Rewriting the cart line only moves the inconsistency one step later, and a cart that disagrees with the confirmation email is worse than one that doesn't.

A quick way to see what you're dealing with before you start: search your theme for products/ + .js, suggest.json and cart.js. Hits outside cart code mean JS work ahead; few or none means the Liquid integration covers you.


Let an AI coding agent do it

If you use Claude Code, Cursor, Copilot or similar, you can hand the whole integration over. Pull your theme locally first (shopify theme pull), open the theme folder in your agent, and paste the prompt below.

The prompt is self-contained — it carries the full data contract, so the agent doesn't need to read this page.

To check the agent's work, use Testing your integration below — force a variation with the cart snippet and confirm every surface changes together.


The data contract

Everything below is stable API. Eraya changes it additively.

1. The product metafield

Namespace

app--258333999105

Key

product_test

Type

json

Written

when a test starts, on every product in the test

Cleared

when the test stops (written as {})

Shape:

Rules worth knowing before you write code against it:

  • The control never has an entry. Falling through to the real product is the control, so omitting it is what lets Liquid treat "no entry" as "no override" without a separate flag.

  • Keys are omitted, never empty. A variation that overrides only the title has no media key at all. This matters — see the empty-array gotcha.

  • variations is keyed by variation id, matching the cart attribute's value.

  • v is the payload version. Gate on it, so a future v: 2 renders the real product on an un-updated theme instead of breaking.

2. The cart attribute

The SDK writes it. You only read it.

3. The media reference pools (optional)

Namespace
Keys
Type

app--258333999105

test_media_1, test_media_2, test_media_3, test_media_4

list.file_reference

Only needed if your tests use images from the Shopify Files library rather than images already attached to the product.

Liquid can turn a media id back into a renderable object only if that media belongs to the product being rendered. An image pulled from Files resolves to nothing, and the gallery silently falls back to the control. The pools fix that: a list.file_reference metafield resolves any file into a media object, attached to the product or not. They're sharded across four keys for capacity.


Step 1 — The resolver block

This is the core. It goes at the top of every file that renders a tested product surface, before anything is output.

Every path that isn't "this visitor is bucketed into a variation that overrides this product" leaves eraya_entry nil and eraya_title / eraya_description holding the real values. The block is safe to add before you've built anything else — with no test running it's a no-op.

In snippets the product variable may be named something else. In card-product.liquid it's card_product; in cart files it's item.product. Substitute accordingly — the rest of the block is identical.

Then replace the render sites in that file:

Including the ones that are easy to miss: | escape in alt and aria-label attributes, data- attributes, and anything feeding structured data.


Step 2 — The media pool

Skip this if your tests only ever use images already attached to the product.

Pool assembly uses paginate, which is a block tag — it cannot live inside a {%- liquid -%} tag, so it sits between the resolver block and the gallery block:


Then replace product.media with eraya_media throughout the file's gallery logic — the loop, media_count, the where: 'media_type' filters and the thumbnail list.

Note the resolution order: pool first, product second. And note that the whole thing fails open — a stale or missing pool renders the real gallery, never a blank one.


Only needed if your tests bind images to specific variants.

Themes that hide variant images

Dawn-family themes have a hide_variants setting that strips variant images from the gallery. An explicit gallery override has to win over that rule, or it will strip images the merchant deliberately chose to show:


Step 5 — The variant JSON blob

Themes serialise product.variants | json into the page for the variant picker to read. If the gallery changed but that blob still points at a removed slide, the picker's switchTo throws — taking price and availability updates down with it.

The fix is to rebuild the blob per variant rather than string-matching ids:

Then output {{ eraya_variants_json }} where the theme had {{ product.variants | json }}.

The limit: 250 is a deliberate ceiling — products above it fall back to the untouched blob rather than bloating the page.


Which surfaces to patch

The title a shopper sees in the cart has to match the one they saw on the product page, or the experiment is measuring a confusing experience rather than a clean variation. Here's the full surface map, using Dawn file names as the reference:

Surface
Dawn file
What to apply

Product page

sections/main-product.liquid

Title, description, gallery, variant JSON

Media gallery

snippets/product-media-gallery.liquid

Gallery, variant featured media

Media lightbox

snippets/product-media-modal.liquid

Gallery, variant featured media

Variant picker

snippets/product-variant-picker.liquid

Variant JSON blob

Collection / grid cards

snippets/card-product.liquid

Title, first image

Thumbnails

snippets/product-thumbnail.liquid

Title (alt text, data-shopify-title)

Featured product section

sections/featured-product.liquid

Same as product page

Cart page

sections/main-cart-items.liquid

Title, line image

Cart drawer

snippets/cart-drawer.liquid

Title, line image

Add-to-cart notification

sections/cart-notification-product.liquid

Title, image

Predictive search

sections/predictive-search.liquid

Title, image


Gotchas

These four cost real debugging time on the reference integration. All of them fail silently.

Gotcha 1: id type coercion

Payload ids are JSON strings. media.id in Liquid is an integer. Liquid's == is type-sensitive, so where: 'id', "31234567890" matches nothing at all.

Gotcha 2: an empty array is truthy

if eraya_entry.media is true even for []. The guard only works because the writer omits the key entirely when there's no gallery override. If you build your own payload writer, never emit [].

Gotcha 3: variant_media keys are strings

variant.id is an integer, so index with an appended empty string:

Gotcha 4: paginate can't live inside {% liquid %}

It's a block tag. Pool assembly must sit between your liquid tags, not inside one.


JavaScript API

If you have custom JS — a third-party review widget, a bundle builder, a custom gallery — that needs to know the visitor's assignment, the SDK exposes it:

These live on the product-test engine, not on window.Eraya directly. They're contract — shapes change additively.

getProductTestAssignment(productId) — synchronous

Which variation this visitor is in, or null if the product isn't under test or they aren't bucketed yet. Everything it reads is already in memory, so there's no network cost — use it for analytics tagging or to branch on the arm.

getProductTest(productId) — async

The assignment plus whatever the variation overrides for that product.

Async because the override content isn't in the page config — the shop-level metafield carries assignment data only, so this reads Eraya's app proxy at /apps/eraya-proxy/config.json. Results are memoised per product, and concurrent calls for the same product share one request.

Control returns a result with no override fields set. That's a meaningful answer ("you're in control, nothing is changed"), not an absence — and it costs no network call.

getProductVariantMedia(productId, variantId) — async

Patching a JSON-rendered surface

This is what you use for a quick-view modal or a JSON search dropdown — anywhere the markup came from /products/<handle>.js or /search/suggest.json and therefore carries the real product data.

Call it right after you inject the JSON-rendered markup, before it becomes visible — the same reason the Liquid path exists is the reason a visible swap is worse than none.

Mark the elements you intend to override (data-eraya-title, data-eraya-image above) rather than matching on class names — theme classes change with every update, and a selector that silently stops matching gives you a half-applied variation with no error.


Testing your integration

Force a variation

Set the cart attribute directly and reload:

Get both ids from the test's detail page in Eraya. Clear with null to return to whatever the SDK assigns you.

Check each one

Check
Expected

No test running

Every surface renders the real product

Control bucket

Every surface renders the real product

Variant bucket

Title, description and gallery all change together

Add to cart in a variant

Cart line shows the variation's title and image

Switch variants

Price and availability still update (Step 5 works)

Test stopped

Everything reverts with no theme change

That last one matters: stopping a test clears the metafield, so Liquid falls straight back to the real product. Ending an experiment never requires a theme edit.

Verify attribution

Place a test order in a variant bucket and confirm it appears on the test's results page. If traffic is climbing but both arms show zero orders, the cart attribute prefix is wrong — see the warning in the data contract.


Ending an integration

The Liquid is inert when no test is running: no metafield means no eraya_entry, which means the real product renders. You can safely leave it in place between tests — that's the intended steady state.

To remove it entirely, delete the resolver, pool and gallery blocks and revert eraya_title / eraya_description / eraya_media / eraya_variants_json back to their product.* originals.

Last updated