Money, images, WhatsApp and Meta events
These helpers are for presentation. They do not change what a shopper is charged, which image the merchant chose, or which conversion the server records.
Money
formatMoney(amount, { symbol, locale, decimals }) is the one formatter:
Code
It formats the number for the locale, then writes the symbol after the amount in every language, as Algerian prices are written, joined by a no-break space so the two never wrap apart:
locale | symbol | Result |
|---|---|---|
ar | دج | 5,000 دج |
ar-DZ | دج | 5.000 دج |
fr | DA | 5 000 DA (the group separator is a narrow no-break space) |
en | DA | 5,000 DA |
Digits and separators are the runtime's Intl data for that locale. Without a
symbol the result is the amount alone.
The default is zero decimals: Algerian dinar prices are whole amounts. Pass
decimals: 2 only when the store actually uses fractional amounts.
Always pass config.locale and config.currencySymbol, not a hardcoded
"DZD" and not the primary language. currencySymbol is already in the
page's language (دج in Arabic, DA in French).
amount is in the store's whole currency unit. Do not divide by 100.
Images
Every image the server answers is a MediaImage, { key, url, width, height }
(width and height are null when unknown). config.media is the store's
MediaDelivery, { kind, origin }: where every key is served from and how
that origin resizes. The helpers take it, so one theme works on every host a
store can use.
config.media.kind | Resizing |
|---|---|
imagekit | ImageKit's ?tr= transformations |
cloudflare | Cloudflare's /cdn-cgi/image/…, which falls back to the original when a resize fails |
direct | None: images are served as stored |
Never build an image URL from a host and a key by hand, and never assume one host: the same theme serves stores on all three.
The product's representative image
productImageSource(product) picks the URL of the merchant's cover, or the
first usable gallery image, or null:
Code
coverImage is frequently null even when a product has photos. Reading it
directly leaves empty squares in cards and baskets. It accepts a Product or
a listed product.
A photo at the size the page shows it
mediaImageUrl(url, params, delivery) asks the store's resizer for one size:
Code
params takes width, height, quality (1–100), format (auto lets the
host pick the best format the browser accepts) and fit (scale-down,
contain, cover, crop, pad, squeeze, read like CSS object-fit).
It answers null when it cannot resize: no delivery, a direct delivery, or
a URL the delivery does not serve. Show the original URL then, as the
?? src above does. That fallback is why a store on direct still renders.
Width-based sources
A responsive grid needs width descriptors with sizes, so a phone downloads a
phone-sized file. mediaImageSrcset(src, widths, { delivery, ratio, quality })
gives them:
Code
- With
ratio(height ÷ width), each size is cropped to it (cover). Without it, the photo keeps its shape and is never enlarged (scale-down). - Every size asks for
format: "auto". - It answers
nullexactly whenmediaImageUrlwould, or when no width is a positive whole number. Leavesrcsetout then. - Set
widthandheighton the<img>to reserve its box: that prevents layout shift. A gallery's first image may useloading="eager"; the rest stay lazy.
Image keys
A media key, such as products/3f2a9c.webp, is an image's identity in the
store's media. Its URL depends on the store's delivery, so it is derived at
render and never stored:
Code
isMediaKey(value) accepts a key that starts with one of the store's media
folders (products/, landing/, reviews/), followed by segments of letters,
digits, ., _ and - that each start with a letter or digit, 512
characters at most. mediaUrl answers null for anything else, and without a
delivery.
An image the merchant picked
A theme's image field stores a media key, an alt text and a focal point.
pictureOf(image, delivery) turns it into what an <img> needs: its URL, its
alt text (empty when the merchant wrote none), its size when known, and the
focal point as object-position (30% 70%):
Code
It answers null when nothing is picked, the key is not a media key, or there
is no delivery: render the section without the image then. Resize
picture.src with mediaImageUrl like any other photo.
The WhatsApp button App owns the store's number, its texts and where the
floating launcher shows. A theme owns only how it looks, and its own words for
every text the merchant left empty. whatsAppContact(config, page, words) is
the one way to read it:
Code
It answers null when the store has no WhatsApp: the merchant has not set up
the App, or the store cannot be read. Render no launcher and no WhatsApp link
then, and never a fallback of your own.
What it resolves
| Field | Value |
|---|---|
href | The wa.me link to the store's number, with this page's message in it |
launcherLabel, caption, welcomeMessage | The merchant's text in the page's language, else your launcher, caption, welcome (each may be null: show nothing there) |
ctaLabel | The merchant's, else your cta: the panel's button, and the text of any other WhatsApp link |
orderButtonLabel | The merchant's, else your orderButton: the button on an order's confirmation or tracking page |
agentName | The merchant's agent, else the store's name |
avatarUrl, accent (whatsapp or primary), position (right or left), attention | How the merchant set the launcher up |
surfaces | Where the floating launcher shows: pass it to showsWidgetOn |
The message is the page's own template, else the merchant's general one, else your message for that kind of page:
page.kind | Use it on | Template it takes first |
|---|---|---|
general | Every page about no product or order: home, listings, contact, the footer | The merchant's general message |
product | A product or landing page; pass product (its name) | The merchant's product message |
order | The order confirmation and order tracking; pass order (its number) | The merchant's order message |
The message rules are the SDK's, applied for you:
{store},{product},{url}and{order}are filled in.{product}falls back to the store's name when there is no product;{order}becomes nothing when there is no order. An unknown placeholder stays as written.{url}ispage.urlwithout its ad-click parameters (utm_*,fbclid,gclidand the like): a shopper arriving from an ad does not hand the merchant a tracking-laden link.- The message is capped at 900 characters (
MAX_RESOLVED_PREFILL_LENGTH), cut at a word.
Where to show the launcher
Code
showsWidgetOn decides the floating launcher only. A WhatsApp link elsewhere
(a footer, a contact card, the order button) shows whenever contact is not
null. Pass the path without its language prefix: /fr/products/robe
matches no page and shows no launcher. The switches and their pages are
listed in Store config.
Keep it in one place
Write your words once, in one module that wraps whatsAppContact, and call
it from every place that links to WhatsApp: the launcher, the footer, the
contact page, the order button. Never write a wa.me link or read
config.whatsapp anywhere else. The theme starter
has the module (src/lib/whatsapp.ts, storeWhatsApp) and a guard test that
fails on any other WhatsApp link.
Meta browser events
The SDK builds Meta payloads and wraps the pixel function:
Code
A product payload carries content_ids, content_type: "product", value and
currency: "DZD". A cart payload adds contents and num_items.
The cart builders are pure and return null for no lines — for
AddToCart, a tap that added nothing because the basket is full. When the
shopper starts checkout, report the whole basket with its value:
Code
Use trackSingle(pixelId, event, params, { eventID }) when the sale belongs to
one pixel. The Purchase event is different: its pixel and event are the
server's decision. Read them from orders.tracking and send nothing when it
returns null — see Checkout and OTP.
A browser event is not the conversion
The browser event and the server's Conversions API event are two halves of one
conversion. They must carry the same eventID and go to the same pixel. Do not
invent a pixel, an event name or an event ID on the thank-you page.
AddToCart reports what the tap put in — addedLines(before, after) — never the
whole basket: a shopper adding one piece to a basket of five did not add six.
A presentation checklist
- Prices use
config.localeandconfig.currencySymbol. - Product images come from
productImageSource, notcoverImagealone. - Every resized URL comes from
mediaImageUrlormediaImageSrcsetwithconfig.media, with the original URL as the fallback. <img>carrieswidth,heightandalt.- Every WhatsApp link comes from
whatsAppContact, through one module that holds the theme's words; the floating launcher also checksshowsWidgetOn. - Meta payloads come from the builders; Purchase comes from
orders.tracking.