SSR live pricing: the theme snippet and metafield contract (Beta)
Updated Sep 25, 202611 min read
On this page (11)
Client-side live pricing rewrites prices after the page loads, so shoppers see the original price for a moment first. SSR live pricing removes that flash: Hype writes the campaign's discount rules to a shop metafield, and your theme's price snippet applies them in Liquid, before the HTML leaves Shopify.
The trade is a one-time theme code edit, and much narrower campaign support.
When to use it
Use it when you need discounted prices in the initial HTML — no flash on load, and prices that are present for crawlers. Use client-side live pricing instead when you cannot edit theme code, or when the campaign is anything other than an automatic Multi Products Discount.
| SSR live pricing | Client-side live pricing | |
|---|---|---|
| How prices change | Liquid renders them | JavaScript replaces them after load |
| Flash of the original price | No | Yes, briefly |
| Theme code edit | Required | None |
| Prices in the initial HTML | Yes | No |
| Campaign types | Automatic Multi Products Discount | Multi Products Discount, Order Discount, Volume Discount |
Turning it on
The toggle lives in the campaign's three-dot menu, as Enable SSR live pricing (Beta). It appears only when the campaign is Active, uses the Automatic discount method, and is a Multi Products Discount.
Enabling it:
- writes the campaign's rules to the shop metafield
hype.discount_rates, - turns SSR live pricing off on every other automatic campaign in the shop, so only one campaign's rates exist at a time.
Client-side live pricing is left as it is: both can be on for one campaign, see Using SSR live pricing together with live pricing.
The metafield is re-synced whenever you edit the campaign while SSR is on. It is cleared when you disable the toggle, delete the campaign, deactivate it, or change its discount method. If the data ever looks stale, toggle SSR live pricing off and on.
Using SSR live pricing together with live pricing
Both can be on for the same campaign. Use that when part of the storefront is not rendered by Liquid — a search or collection grid built by a JavaScript app, for example a Vue component fed by an external search service. Liquid never runs there, so the SSR snippet cannot price those cards; client-side live pricing rewrites them in the browser instead.
The catch is overlap. Live pricing rewrites whatever the schema matches, including elements the SSR snippet already priced. On those elements shoppers see the price repaint after the page loads, and for amount discounts converted into another currency the two paths can round differently — €52,60 server-side can briefly become €52,58 client-side. The toggle dialogs warn about this when the other feature is already on.
The fix is the data-hype-ssr attribute described under The theme edit: the storefront script skips any element carrying it, so a broad schema is safe as long as the snippet sets it. If the snippet predates the attribute, scope the schema to the areas Liquid does not render instead:
- Give app-rendered cards their own
product-cardrule with aparentSelectorthat only matches those cards, andtemplate: "collection"when the grid only appears on search or collection pages. - Do not keep a
product-pagerule, or anall-template card rule that matches the theme's own price markup, while SSR is on for that campaign. - Check with Test Live pricing in Settings: anything that repaints on a Liquid-rendered page is a rule that overlaps.
The metafield contract
Read it in Liquid as shop.metafields.hype.discount_rates.value. It is a JSON metafield on the shop.
| Field | Description |
|---|---|
version | Payload contract version. The current payload is 3. |
campaignId | The campaign the payload belongs to |
targetMarketIds | Market IDs where the rules apply. An empty array means every market. |
b2bCustomers | include, exclude or only. Whether buyers logged in as a company contact are eligible. Check it with customer.b2b? — localization.market.id never resolves to a B2B market, so the market list alone can't express this. Missing on older payloads; treat as include. |
activeDates.startsAt | ISO 8601 UTC instant the campaign starts — the same value Shopify holds as the discount's start. Treat a missing value as already started. |
activeDates.endsAt | ISO 8601 UTC instant the campaign ends, or null when it has no end date. Treat missing as no end. |
discounts[] | The campaign's rules, in order |
discounts[].type | percentage or amount |
discounts[].value | For percentage, a number: 20 means 20% off. For amount, an object keyed by market ID, with primary holding the shop's primary-currency value. |
discounts[].allProducts | true when the rule covers the whole catalog |
discounts[].productIds | Products the rule covers |
discounts[].collectionIds | Collections the rule covers |
discounts[].excludedProductIds | Products the rule skips |
discounts[].excludedCollectionIds | Collections the rule skips |
Scheduled campaigns switch themselves on and off.: the snippet compares activeDates against 'now', so a campaign with a future start date can have SSR enabled ahead of time and needs no theme edit when it starts or ends. 'now' is evaluated when Liquid renders and Shopify caches storefront pages, so the switch can lag by a few minutes for anonymous visitors.
Every ID is a numeric string with the GID prefix stripped: gid://shopify/Product/123 is stored as "123". Amount values are in currency units, not cents — 5 means 5.00.
{ "version": 3, "campaignId": "c1f7…", "targetMarketIds": ["12345678"], "b2bCustomers": "include", "activeDates": { "startsAt": "2026-09-20T22:00:00.000Z", "endsAt": null }, "discounts": [ { "type": "percentage", "value": 20, "allProducts": false, "productIds": ["7891011"], "collectionIds": ["445566"], "excludedProductIds": [], "excludedCollectionIds": ["778899"] } ]}Product matching. A product matches a rule when allProducts is true, or its ID is in productIds, or it belongs to a collection in collectionIds — and it is not in excludedProductIds and belongs to no collection in excludedCollectionIds. When several rules exist, the first match wins.
The theme edit
Step 1 — find your price snippet. In Dawn it is snippets/price.liquid. In other themes, look for the snippet that assigns price and compare_at_price and renders them through the money filter.
Step 2 — insert the block after those assignments and before the | money formatting lines:
{% liquid comment Hype SSR: read the shop metafield and override price/compare_at_price when the current product matches an active discount rule. endcomment assign hype_data = shop.metafields.hype.discount_rates.value assign hype_matched_discount = nil assign hype_market_id = localization.market.id | append: '' assign hype_market_allowed = false if hype_data if hype_data.targetMarketIds == blank assign hype_market_allowed = true elsif hype_data.targetMarketIds contains hype_market_id assign hype_market_allowed = true endif if hype_data.b2bCustomers == 'exclude' and customer.b2b? assign hype_market_allowed = false endif if hype_data.b2bCustomers == 'only' and customer.b2b? != true assign hype_market_allowed = false endif assign hype_now_ts = 'now' | date: '%s' | plus: 0 if hype_data.activeDates.startsAt != blank assign hype_starts_ts = hype_data.activeDates.startsAt | date: '%s' | plus: 0 if hype_now_ts < hype_starts_ts assign hype_market_allowed = false endif endif if hype_data.activeDates.endsAt != blank assign hype_ends_ts = hype_data.activeDates.endsAt | date: '%s' | plus: 0 if hype_now_ts > hype_ends_ts assign hype_market_allowed = false endif endif endif if hype_market_allowed and hype_data.discounts.size > 0 assign pid_string = product.id | append: '' for hype_discount in hype_data.discounts assign hype_is_excluded = false for excluded_pid in hype_discount.excludedProductIds if pid_string == excluded_pid assign hype_is_excluded = true break endif endfor unless hype_is_excluded for excluded_cid in hype_discount.excludedCollectionIds for col in product.collections assign col_id_string = col.id | append: '' if col_id_string == excluded_cid assign hype_is_excluded = true break endif endfor if hype_is_excluded break endif endfor endunless if hype_is_excluded continue endif if hype_discount.allProducts assign hype_matched_discount = hype_discount break endif for pid in hype_discount.productIds if pid_string == pid assign hype_matched_discount = hype_discount break endif endfor if hype_matched_discount break endif for cid in hype_discount.collectionIds for col in product.collections assign col_id_string = col.id | append: '' if col_id_string == cid assign hype_matched_discount = hype_discount break endif endfor if hype_matched_discount break endif endfor endfor endif assign hype_currency_base = shop.metafields.custom.hype_money_primary.value | times: 1.00 assign hype_exchange_rate = hype_currency_base | divided_by: 1000000.00 if hype_matched_discount assign compare_at_price = price if hype_matched_discount.type == 'percentage' assign hype_multiplier = 100 | minus: hype_matched_discount.value assign price = price | times: hype_multiplier | divided_by: 100 else assign hype_discount_amount = hype_matched_discount.value[hype_market_id] if hype_discount_amount assign hype_discount_cents = hype_discount_amount | times: 100 else assign hype_discount_cents = hype_matched_discount.value.primary | times: hype_exchange_rate | times: 100 endif assign price = price | minus: hype_discount_cents | at_least: 0 endif endif%}Step 3 — check the order. The variables must flow like this:
1. Theme assigns price and compare_at_price2. → the Hype block goes here ←3. Theme assigns money_price = price | money4. Theme renders its markupBecause the block overrides price and compare_at_price before formatting, your theme's existing sale markup, classes and strikethrough styling all keep working.
Two details make this work everywhere rather than only on the product page. product.collections is available in product cards, collection pages, search results and the cart, so collection rules resolve anywhere. And product.id and collection.id are numbers in Liquid, so the block appends an empty string to compare them with the metafield's string IDs. product.price is in cents, which is why amount values are multiplied by 100.
Mark the price container as server-priced
Add data-hype-ssr to the element that wraps the price the snippet just computed, only when a rule matched:
<div class="price{% if on_sale %} price--on-sale{% endif %}"{% if hype_matched_discount %} data-hype-ssr{% endif %}>The storefront script never rewrites an element carrying this attribute, or anything inside one. That is what makes running live pricing alongside SSR safe: prices Liquid discounted are left alone, and because the attribute is only present when a rule matched, prices SSR could not resolve still fall through to live pricing.
Optional: converting amount discounts for other currencies
Skip this section if every rule in your campaign is a percentage, or if your store sells in one currency. 20% off is 20% off in any currency, and a single-currency store never converts anything. You can also skip it if you set a per-market value for every market on each amount rule in the campaign, because the block uses that value directly.
The section matters in one case: an amount rule, a shopper browsing in a currency that is not your store's primary currency, and no per-market value for that market in the campaign. Then the block has to convert the primary-currency amount, and Liquid exposes no exchange rate to do it with. Shopify converts product.price for you, but it never tells you the rate it used.
How the trick works
Shopify converts money-type metafields into the shopper's presentment currency, the same way it converts product prices. So a money metafield with a known base value gives you the rate: divide what Shopify renders by what you stored.
The block expects a shop metafield custom.hype_money_primary holding 1000000 in your store's currency. If your store sells in USD and a shopper is browsing in EUR at 0.92, Shopify renders it as 920000, and 920000 divided by 1000000 is 0.92. A 5.00 USD amount rule then becomes 4.60 EUR. The rate you get this way includes Shopify's currency conversion markup, which is what you want, because product.price carries the same markup.
These are the lines in the block that do it:
assign hype_currency_base = shop.metafields.custom.hype_money_primary.value | times: 1.00assign hype_exchange_rate = hype_currency_base | divided_by: 1000000.00and this is the branch that uses the rate, only when the rule has no value for the shopper's market:
assign hype_discount_cents = hype_matched_discount.value.primary | times: hype_exchange_rate | times: 100Create the metafield yourself
Hype does not create this metafield, and it never will: it lives in your store's own custom namespace, not in Hype's, and it is a store-level setting that belongs to you. Add it once by hand:
- In the Shopify admin, go to Settings, then Custom data, then Shop.
- Click Add definition.
- Name it anything you like. Set Namespace and key to exactly
custom.hype_money_primary. - Choose the type Money and save the definition.
- Back on the Shop metafields page, find the new field and enter 1000000 in your store's primary currency. Save.
The value has to be exactly one million. The block divides by 1000000.00, so any other base gives a wrong rate. Use the shop's primary currency for the value; that is what makes the division come out as the rate from primary to presentment.
If you leave it out
Without the metafield, shop.metafields.custom.hype_money_primary.value is empty, the base comes out as 0, the rate comes out as 0, and the fallback branch produces a zero discount. Percentage rules and markets with their own value are unaffected, but an amount rule shown in an unconfigured currency silently shows the full price. If you know you will never rely on the fallback, remove the two hype_currency_base and hype_exchange_rate lines and the else branch of the amount calculation so the block does not read a metafield you did not create.
Related
Was this article helpful?
Related articles
Still stuck?
Ask a follow-up and we'll see it came from “SSR live pricing: the theme snippet and metafield contract (Beta)”.
Opens chat with your question and a link to this article.