Hype Discounts

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 pricingClient-side live pricing
How prices changeLiquid renders themJavaScript replaces them after load
Flash of the original priceNoYes, briefly
Theme code editRequiredNone
Prices in the initial HTMLYesNo
Campaign typesAutomatic Multi Products DiscountMulti 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-card rule with a parentSelector that only matches those cards, and template: "collection" when the grid only appears on search or collection pages.
  • Do not keep a product-page rule, or an all-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.

FieldDescription
versionPayload contract version. The current payload is 3.
campaignIdThe campaign the payload belongs to
targetMarketIdsMarket IDs where the rules apply. An empty array means every market.
b2bCustomersinclude, 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.startsAtISO 8601 UTC instant the campaign starts — the same value Shopify holds as the discount's start. Treat a missing value as already started.
activeDates.endsAtISO 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[].typepercentage or amount
discounts[].valueFor 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[].allProductstrue when the rule covers the whole catalog
discounts[].productIdsProducts the rule covers
discounts[].collectionIdsCollections the rule covers
discounts[].excludedProductIdsProducts the rule skips
discounts[].excludedCollectionIdsCollections 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 markup

Because 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.00

and 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: 100

Create 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:

  1. In the Shopify admin, go to Settings, then Custom data, then Shop.
  2. Click Add definition.
  3. Name it anything you like. Set Namespace and key to exactly custom.hype_money_primary.
  4. Choose the type Money and save the definition.
  5. 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 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.