For merchants who need full control over the HTML layout or want to build a completely custom loyalty page, JeriCommerce provides standalone Liquid section templates that load the loyalty page engine directly from the CDN. This guide is aimed at developers building custom storefront experiences for their merchants.
If the merchant is happy with the default look and just wants to tweak colors and copy, use the default template guide instead.
JeriCommerce provides two ready-made CDN templates. Copy the one that best fits your needs:
You can obtain these from the JeriCommerce admin under Loyalty Page → Get template code, or copy directly from the repository files.
Create a JSON page template that references your section:
// templates/page.loyalty.json
{
"sections": {
"loyalty": {
"type": "jeri-loyalty"
}
},
"order": ["loyalty"]
}Alternatively, add the section to an existing page template via the theme editor: Customize → Add section.
Go to Shopify Admin → Pages → Add page → Select the "loyalty" template.
CDN templates load the same JavaScript that powers the theme app extension block — bundled, minified, and always up to date:
<!-- Always the latest version --> <script src="https://cdn.jericommerce.com/shopify/loyalty-page.js" defer></script> <link rel="stylesheet" href="https://cdn.jericommerce.com/shopify/loyalty-page.css" />
The engine auto-detects the shop domain from the data-shop attribute on the root element, or falls back to window.Shopify.shop.
The template engine uses two systems to bind API data to your HTML:
1. Variable Interpolation — {varName}
Write {varName} tokens anywhere in your HTML text content. The JS engine walks all text nodes and replaces them with API data. This is the primary way to display data.
<h3>{tier.name}</h3>
<span>{tier.requiredPoints}</span>
<span>{earning.points}</span>Inside repeating templates (tiers, earnings), variables are scoped to the template data. Syntax uses single curly braces {key} — no conflict with Liquid ({{ }}), JS template literals (${ }), or CSS.
There are also global variables (like {programName}, {currencySymbol}) that work in any text node across the entire template — not just inside repeating templates. See the Global Template Variables section below for the full list.
2. Data Attributes — data-jeri
Used for non-text operations that can’t be expressed as text interpolation:
| Attribute | Purpose | Example |
|---|---|---|
| data-jeri="name" on <template> | Defines a repeating row template | <template data-jeri="tiers"> |
| data-jeri="name" on container | Receives cloned template rows | <div data-jeri="tiers"> |
| data-jeri="key" on <img> | Sets src attribute (hides if empty) | <img data-jeri="earning.iconUrl"> |
| data-jeri-hide-if="value" | Hide element when data matches value | data-jeri-hide-if="1" |
| data-jeri-html="key" | Inject raw HTML content | <p data-jeri-html="tier.content"> |
| data-jeri-group="name" | Group container (hidden when empty) | data-jeri-group="purchases" |
| Need | Use |
|---|---|
| Display text/numbers | {varName} in text content |
| Mix variables with custom copy | Earn {earning.points} with {earning.title} |
| Set image src | data-jeri="key" on <img> |
| Hide based on value | data-jeri-hide-if |
| Inject rich HTML content | data-jeri-html="key" |
| Define a repeating row | <template data-jeri="name"> + <div data-jeri="name"> |
These 7 global variables can be used in any text node inside #jeri-loyalty-page-root. Write them with {variableName} syntax — the engine replaces them automatically with data from the program configuration.
| Variable | Type | Description | Example |
|---|---|---|---|
| {programName} | string | Name of the loyalty program | "My Rewards" |
| {currencySymbol} | string | Currency symbol for the program’s currency, resolved via the browser’s Intl.NumberFormat | "$", "€", "£" |
| {currencyCode} | string | ISO currency code for the program | "USD", "EUR", "GBP" |
| {balanceName} | string | Plural name for points | "Points", "Stars" |
| {singularBalanceName} | string | Singular name for points | "Point", "Star" |
| {tierBalanceName} | string | Plural name for tier points | "Points" |
| {singularTierBalanceName} | string | Singular name for tier points | "Point" |
<h1>Welcome to {programName}!</h1>
<p>Earn {balanceName} with every {currencySymbol} you spend</p>
<p>Prices are in {currencyCode}</p>Global variables work alongside scoped variables ({earning.*}, {tier.*}). Inside repeating templates, both global and scoped variables are resolved.
Note: The {currencySymbol} variable uses the browser’s native Intl.NumberFormat to resolve the correct symbol for the program’s currency code. This means the symbol adapts to the actual program currency — $ for USD, € for EUR, £ for GBP, and so on.
The variable {hero.programName} has been removed. Use {programName} instead — it works everywhere in the template, not just in the hero section.
Used inside the <template data-jeri="earning-row"> template.
| Field | Type | Description |
|---|---|---|
| earning.type | string | Flow type: purchase, social, referral, profile, verify, wallet, scan, link |
| earning.title | string | Human-readable title for the earning rule |
| earning.description | string | Detailed description of how to earn |
| earning.points | string | Compact points display: "+100" for fixed points, "10%" for purchase return rate < 1, "x2" for purchase multiplier ≥ 1 |
| earning.iconUrl | string | Icon URL (set via data-jeri on <img>, empty if no icon). Social networks use per-network icons (facebook.svg, instagram.svg, etc.) |
Used inside the <template data-jeri="tiers"> template.
| Field | Type | Description |
|---|---|---|
| tier.name | string | Tier name (e.g., "Gold", "VIP") |
| tier.content | string | Tier description/benefits — may contain HTML, use data-jeri-html="tier.content" |
| tier.image | string | Tier image URL (set via data-jeri on <img>) |
| tier.requiredPoints | string | Points needed (e.g., "1,000 tier points"). Uses tierBalanceName with singular/plural |
| tier.factor | string | Points multiplier (e.g., "2"). Auto-hidden when "1" via data-jeri-hide-if |
| tier.id | string | Internal tier ID |
Every custom template must include this root element with the required attributes:
<div
id="jeri-loyalty-page-root"
class="jeri-loyalty-page jeri-loyalty-page--loading"
data-shop="{{ shop.permanent_domain }}"
data-icon-base="https://cdn.jericommerce.com/shopify/"
data-locale="{{ request.locale.iso_code }}"
>
<!-- Your custom HTML here -->
</div>| Attribute | Required | Purpose |
|---|---|---|
| id="jeri-loyalty-page-root" | Yes | JS entry point — the engine looks for this ID |
| class="jeri-loyalty-page" | Yes | CSS scoping + CSS variables |
| data-shop | Yes | Shop domain for API calls |
| data-icon-base | Recommended | URL prefix for earning icons |
| data-locale | Recommended | Store locale for i18n (falls back to "en") |
The jeri-loyalty-page--loading class shows a skeleton and hides content until data loads. JS removes it after data is fetched.
Important: The CSS stylesheet must be loaded before the HTML content (inside the root element) to prevent a flash of unstyled content on first load.
The default template is fully customizable from the Liquid file. You can:
<template data-jeri="earning-row">
<div style="padding: 16px; border-bottom: 1px solid #eee;">
<strong>{earning.title}</strong> — earn {earning.points}
<p>{earning.description}</p>
</div>
</template>
<div data-jeri="earnings-purchases"></div>
<div data-jeri="earnings-actions"></div><template data-jeri="tiers">
<div class="my-tier">
<h3>Tier: {tier.name}</h3>
<p>Requires {tier.requiredPoints} to unlock</p>
<span data-jeri="tier.factor" data-jeri-hide-if="1">
Multiplier: {tier.factor}x
</span>
<div data-jeri-html="tier.content"></div>
</div>
</template>
<section class="jeri-loyalty-page__hero">
<h1 class="jeri-loyalty-page__hero-title">Our Loyalty Program</h1>
<p class="jeri-loyalty-page__hero-subtitle">Join our loyalty program...</p>
<!-- CTA adapts to login state via Liquid -->
<div class="jeri-loyalty-page__hero-cta">
{% if customer %}
{% if block.settings.cta_action == 'widget_rewards' %}
<a href="#jeri=loyalty/rewards" class="jeri-loyalty-page__cta-button jeri-loyalty-page__cta-button--primary">
{{ block.settings.logged_in_cta_label | default: 'See rewards' }}
</a>
{% else %}
<a href="{{ block.settings.logged_in_cta_url | default: '/account' }}" class="jeri-loyalty-page__cta-button jeri-loyalty-page__cta-button--primary">
{{ block.settings.logged_in_cta_label | default: 'See rewards' }}
</a>
{% endif %}
{% else %}
<a href="{{ block.settings.guest_cta_url | default: '/account/login' }}" class="jeri-loyalty-page__cta-button jeri-loyalty-page__cta-button--primary">
{{ 'loyalty_page.hero_cta' | t }}
</a>
{% endif %}
</div>
</section><template data-jeri="tiers">
<div class="jeri-tier-card">
<div class="jeri-tier-card__body">
<div class="jeri-tier-card__top">
<h3 class="jeri-tier-card__name">{tier.name}</h3>
<span class="jeri-tier-card__factor" data-jeri="tier.factor" data-jeri-hide-if="1">
{tier.factor}x
</span>
</div>
<div class="jeri-tier-card__meta">
<span class="jeri-tier-card__points">{tier.requiredPoints}</span>
</div>
<p class="jeri-tier-card__content" data-jeri-html="tier.content"></p>
</div>
</div>
</template>
<!-- Container: cloned cards are appended here -->
<div data-jeri="tiers" class="jeri-tier-grid"></div><!-- Purchase earnings group -->
<div class="jeri-earning-group" data-jeri-group="purchases">
<h3 class="jeri-earning-group__title">Purchases</h3>
<div class="jeri-earning-list" data-jeri="earnings-purchases"></div>
</div>
<!-- Action earnings group -->
<div class="jeri-earning-group" data-jeri-group="actions">
<h3 class="jeri-earning-group__title">Actions</h3>
<div class="jeri-earning-list" data-jeri="earnings-actions"></div>
</div>
<!-- Shared template for both groups -->
<template data-jeri="earning-row">
<div class="jeri-earning-row">
<img class="jeri-earning-row__icon" data-jeri="earning.iconUrl" alt="" width="48" height="48" loading="lazy" />
<div class="jeri-earning-row__content">
<h3 class="jeri-earning-row__title">{earning.title}</h3>
<p class="jeri-earning-row__description">{earning.description}</p>
</div>
<span class="jeri-earning-row__points">{earning.points}</span>
</div>
</template>
Here is a complete minimal custom template to use as a starting point:
<!-- CDN Scripts -->
<script src="https://cdn.jericommerce.com/shopify/loyalty-page.js" defer></script>
<link rel="stylesheet" href="https://cdn.jericommerce.com/shopify/loyalty-page.css" />
<div
id="jeri-loyalty-page-root"
class="jeri-loyalty-page jeri-loyalty-page--loading"
data-shop="{{ shop.permanent_domain }}"
data-icon-base="https://cdn.jericommerce.com/shopify/"
data-locale="{{ request.locale.iso_code }}"
>
<!-- Hero -->
<section class="jeri-loyalty-page__hero">
<h1 class="jeri-loyalty-page__hero-title">Our Loyalty Program</h1>
<p class="jeri-loyalty-page__hero-subtitle">Join and start earning rewards today.</p>
<div class="jeri-loyalty-page__hero-cta">
{% if customer %}
<a href="/account" class="jeri-loyalty-page__cta-button jeri-loyalty-page__cta-button--primary">
See rewards
</a>
{% else %}
<a href="/account/login" class="jeri-loyalty-page__cta-button jeri-loyalty-page__cta-button--primary">
Join now
</a>
{% endif %}
</div>
</section>
<!-- Tiers -->
<template data-jeri="tiers">
<div>
<h3>{tier.name}</h3>
<p>{tier.requiredPoints}</p>
<span data-jeri="tier.factor" data-jeri-hide-if="1">
Multiplier: {tier.factor}x
</span>
<div data-jeri-html="tier.content"></div>
</div>
</template>
<div data-jeri="tiers"></div>
<!-- Earnings -->
<template data-jeri="earning-row">
<div>
<img data-jeri="earning.iconUrl" alt="" width="32" height="32" />
<strong>{earning.title}</strong>
<span>{earning.points}</span>
<p>{earning.description}</p>
</div>
</template>
<div data-jeri-group="purchases">
<h3>Purchases</h3>
<div data-jeri="earnings-purchases"></div>
</div>
<div data-jeri-group="actions">
<h3>Actions</h3>
<div data-jeri="earnings-actions"></div>
</div>
<!-- Loading skeleton -->
<div class="jeri-loyalty-page__skeleton">
<div class="jeri-loyalty-page__skeleton-hero"></div>
</div>
</div>
Visual template — Best for brands that want a polished, marketing-style loyalty page with hero section, how-it-works steps, earning rows with SVG icons, tier cards, and full loading skeleton.
Minimal template — Best for stores that want a clean, data-first approach with compact list layout, inline CSS, and no external stylesheet dependency beyond the JS engine.
Both templates use the same JS engine and data binding system. The only difference is the HTML structure and visual design. You can start from either one and customize freely.
Earning row icons are SVG files named by type. The icon URL is computed as data-icon-base + name + .svg.
| Icon File | Used For |
|---|---|
| purchase.svg | Purchase earning flows |
| facebook.svg | Follow on Facebook |
| instagram.svg | Follow on Instagram |
| tiktok.svg | Follow on TikTok |
| x.svg | Follow on X |
| youtube.svg | Follow on YouTube |
| referral.svg | Refer a friend |
| profile.svg | Complete profile |
| wallet.svg | Install wallet pass |
| scan.svg | Scan in store |
| verify.svg | Verify account |
| link.svg | Visit link |
In CDN mode, the icon base URL is hardcoded to https://cdn.jericommerce.com/shopify/. If data-icon-base is missing or an icon doesn’t exist, the <img> element is hidden automatically.
The earning.points field uses a compact format depending on the earning type:
| Earning type | Format | Example |
|---|---|---|
| Purchase (rate < 1) | Percentage return | 10% |
| Purchase (rate >= 1) | Multiplier | x2 |
| All others (social, referral, profile, etc.) | Fixed points | +100 |
For referral, only the referrer’s points are shown in the badge. The friend’s reward is mentioned in the description text.
The purchase earning description now uses the program’s actual currency symbol dynamically (e.g., "Earn x3 Points per $ spent" for USD, "Earn x3 Points per € spent" for EUR). The currency is resolved from the program configuration using the browser’s Intl.NumberFormat.
Four naming properties are resolved from the program config:
| Name | Used for | Example |
|---|---|---|
| balanceName / singularBalanceName | Earning descriptions, titles | "points" / "point" |
| tierBalanceName / singularTierBalanceName | Tier required points | "tier points" / "tier point" |
These are read from program.configurations.wallet (base config). Singular/plural is applied automatically based on the amount (1 = singular, else plural).
These four names are also available as global template variables — {balanceName}, {singularBalanceName}, {tierBalanceName}, and {singularTierBalanceName} — so you can reference them directly in any text node. See the Global Template Variables section above for the full list.
| Setting | Type | Default | Description |
|---|---|---|---|
| Enable Loyalty Page | checkbox | true | Show/hide the entire block |
| Hero Title | text | "Our Loyalty Program" | Main heading |
| Hero Subtitle | textarea | "Join our loyalty..." | Description below heading |
| Guest CTA URL | url | /account/login | Where to send guests |
| Logged-in CTA Action | select | account | account = go to account page, widget_rewards = open rewards widget |
| Logged-in CTA Label | text | "See rewards" | Button text shown to logged-in customers |
| Logged-in CTA URL | url | /account | URL when action is "Go to account page" |
| Show How It Works | checkbox | true | Toggle the 3-step section |
| Show Earning Rules | checkbox | true | Toggle the earnings section |
| Show VIP Tiers | checkbox | true | Toggle the tiers section |
| Primary Color | color | #0f0f0f | Buttons, accents, badges |
| Primary Contrast | color | #fcfcfc | Text on primary backgrounds |
| Secondary Color | color | #f5f5f5 | Card backgrounds |
| Text Color | color | #0f0f0f | Headings and body text |
| Muted Color | color | #888888 | Subtitles, descriptions |
| Custom CSS | textarea | — | Additional CSS overrides |
The "See rewards" CTA (when cta_action is set to widget_rewards) uses #jeri=loyalty/rewards as its href. When the JeriCommerce widget is active on the page, clicking this link opens the widget’s rewards panel. This means:
The page automatically hides elements based on program configuration:
| Element | Condition |
|---|---|
| VIP Tiers section | Hidden when program has no tiers |
| Ways to Earn section | Hidden when program has no active engagement flows |
| Purchases group | Hidden when no purchase-type flows exist |
| Actions group | Hidden when no action-type flows exist |
These are set on :root by the Liquid template based on theme editor color settings:
| Variable | Default | Set by |
|---|---|---|
| --jeri-loyalty-primary | #0f0f0f | Primary Color setting |
| --jeri-loyalty-primary-contrast | #fcfcfc | Primary Contrast setting |
| --jeri-loyalty-secondary | #f5f5f5 | Secondary Color setting |
| --jeri-loyalty-text | #0f0f0f | Text Color setting |
| --jeri-loyalty-muted | #888888 | Muted Color setting |
These resolve the merchant variables and add layout/structural tokens. Override them in Custom CSS for fine-grained control:
| Variable | Default | Purpose |
|---|---|---|
| --jlp-primary | var(--jeri-loyalty-primary, #0f0f0f) | Primary color |
| --jlp-primary-contrast | var(--jeri-loyalty-primary-contrast, #fcfcfc) | Text on primary |
| --jlp-text | var(--jeri-loyalty-text, #0f0f0f) | Body text color |
| --jlp-muted | var(--jeri-loyalty-muted, #888888) | Secondary text color |
| --jlp-font | var(--font-body-family, system stack) | Font family |
| --jlp-border-radius | 16px | Border radius for cards |
| --jlp-space | 16px | Base spacing unit |
| --jlp-border-color | #e8e8e8 | Card/list border color |
| --jlp-card-bg | #fcfcfc | Card background color |
| --jlp-divider | #f0f0f0 | Row divider color |
| --jlp-skeleton-bg | #f5f5f5 | Skeleton loader color |
| --jlp-icon-size | 48px | Earning row icon size |
| --jlp-max-width | 1200px | Page max-width |
.jeri-loyalty-page {
--jlp-space: 12px;
--jlp-border-radius: 8px;
}.jeri-loyalty-page {
--jlp-card-bg: #1a1a1a;
--jlp-border-color: #333;
--jlp-divider: #2a2a2a;
--jlp-skeleton-bg: #222;
}
| Class | Element |
|---|---|
| .jeri-loyalty-page | Root container |
| .jeri-loyalty-page--loading | Loading state (shows skeleton, hides content) |
| .jeri-loyalty-page__hero | Hero section |
| .jeri-loyalty-page__section | Content section |
| .jeri-loyalty-page__section-title | Section heading |
| .jeri-loyalty-page__skeleton | Skeleton wrapper |
| Class | Element |
|---|---|
| .jeri-loyalty-page__hero-title | Hero heading |
| .jeri-loyalty-page__hero-subtitle | Hero description |
| .jeri-loyalty-page__hero-cta | CTA button wrapper |
| .jeri-loyalty-page__cta-button | CTA button base |
| .jeri-loyalty-page__cta-button--primary | Primary CTA variant |
| Class | Element |
|---|---|
| .jeri-loyalty-page__steps | Steps grid (3-column) |
| .jeri-loyalty-page__step | Individual step |
| .jeri-loyalty-page__step-number | Circle with number |
| .jeri-loyalty-page__step-title | Step heading |
| .jeri-loyalty-page__step-text | Step description |
| Class | Element |
|---|---|
| .jeri-earning-group | Group wrapper (Purchases/Actions) |
| .jeri-earning-group__title | Group heading |
| .jeri-earning-list | Bordered list container |
| .jeri-earning-row | Individual earning row |
| .jeri-earning-row__icon | Row icon (<img>) |
| .jeri-earning-row__content | Row text wrapper |
| .jeri-earning-row__title | Row title |
| .jeri-earning-row__description | Row description |
| .jeri-earning-row__points | Points display |
| Class | Element |
|---|---|
| .jeri-tier-grid | Tier cards flex container |
| .jeri-tier-card | Individual tier card |
| .jeri-tier-card__body | Card body |
| .jeri-tier-card__top | Name + factor row |
| .jeri-tier-card__name | Tier name |
| .jeri-tier-card__factor | Multiplier badge (hidden when 1x) |
| .jeri-tier-card__content | Description (rendered as HTML) |
| .jeri-tier-card__points | Required points |
The loyalty page supports multiple languages based on the store’s current locale.
Unsupported locales fall back to English automatically.
There are two layers of i18n:
The store locale is passed to JavaScript via the data-locale attribute on the root element:
<div id="jeri-loyalty-page-root" data-locale="{{ request.locale.iso_code }}" ...>
</div>| File | Purpose |
|---|---|
| locales/en.ts | English translations for JS-side strings (bundled into JS) |
| locales/es.ts | Spanish translations for JS-side strings (bundled into JS) |
| locales/fr.ts | French translations for JS-side strings (bundled into JS) |
| locales/de.ts | German translations for JS-side strings (bundled into JS) |
| locales/it.ts | Italian translations for JS-side strings (bundled into JS) |
| locales/en.default.json | English translations for Liquid-side strings (copied to output locales/) |
| locales/es.json | Spanish translations for Liquid-side strings (copied to output locales/) |
| locales/fr.json | French translations for Liquid-side strings (copied to output locales/) |
| locales/de.json | German translations for Liquid-side strings (copied to output locales/) |
| locales/it.json | Italian translations for Liquid-side strings (copied to output locales/) |