Widgets
An integration can provide widgets: reusable blocks that users insert into their emails, popups, smart content or stats dashboards. You declare them with the @shopimind/integration-kit-js kit, and ShopiMind takes care of the rest.
Nothing to host on your side
A widget is a declaration (JSON) carried by your integration. ShopiMind renders it itself, server-side. You expose no render route and no iframe, your templates never touch the browser, and no partner code is ever executed.
Widgets are declared in the widgets array of your defineIntegration — an array of WidgetDeclaration typed against the kit's contract (@shopimind/integration-kit-js), an exact mirror of ShopiMind's internal model: a non-conforming declaration does not compile.
import { defineIntegration } from '@shopimind/integration-kit-js';
import type { WidgetDeclaration } from '@shopimind/integration-kit-js';
const widgets: WidgetDeclaration[] = [/* … */];
export const myIntegration = defineIntegration({
// …
widgets,
});The WidgetDeclaration contract
Each declaration shares a common base, then specializes along two axes: render_type, then (for HTML) render_mode.
| Field | Type | Applies to | Role |
|---|---|---|---|
key | string | all | Widget identifier/slug (the field is literally named key). |
name | WidgetLocalizedText | all | Displayed name, localized ({ fr?, en?, … }). |
description | WidgetLocalizedText | optional | Localized description. |
icon_url | string | optional | Widget icon. |
preview_image_url | string | optional | Preview image. |
targets | WidgetTarget[] | all (required) | Where to drop it: email_template · popup · smart_content · stats_dashboard. |
render_type | 'image' | 'html' | all (required) | Axis 1: output type. |
render_mode | 'static' | 'dynamic' | html only | Axis 2 (omitted for image). |
image_url_template | string | image | Image URL to templatize. |
html_template | string | html + static | Your HTML. {var=…} left literal. Never any JS. |
css_template | string | html + static (optional) | Your CSS, sanitized at render time. Never any JS. |
renderer_key | string | html + dynamic | Key of a renderer that must exist at ShopiMind. |
default_width | number | optional | Default width. |
config_schema | WidgetConfigSchema | optional | Configuration panel (see below). |
The three render forms
| Form | Fields | Rendering |
|---|---|---|
render_type: "image" | image_url_template | An <img> tag built from your URL. No server-side rendering. |
render_type: "html" + render_mode: "static" | html_template (+ css_template) | Your HTML/CSS, validated then sanitized (no JS). Email-compatible. |
render_type: "html" + render_mode: "dynamic" | renderer_key | Reuses an existing ShopiMind renderer (e.g. spm_countdown). |
What you do alone vs co-designed with ShopiMind
You can ship the image and html + static widgets on your own: no code change on ShopiMind's side. An html + dynamic widget is co-designed with ShopiMind: your declaration only carries a renderer_key, and the matching WidgetRenderer class must be written and registered in ShopiMind's code. Reserve dynamic for logic designed with us (e.g. spm_countdown).
Image widget
ShopiMind does not render the image server-side: the editor substitutes the placeholders in image_url_template and emits the <img> tag. The {var=…} stay literal in the URL and are resolved per recipient at send time.
{
"key": "promo_banner",
"name": { "fr": "Bannière promo", "en": "Promo banner" },
"targets": ["email_template", "popup"],
"render_type": "image",
"image_url_template": "https://votre-service/img?promo={widget.code}&u={var=contact.email}"
}No secret in an image URL
Only the non-sensitive values of the integration config feed {integration.*}. A secret never travels in an image URL nor in rendered HTML.
Static HTML widget (render_mode: "static")
Your html_template (+ optional css_template) is stored with the declaration, then substituted and sanitized at render time (no <script>, email-compatible). Real-world example (loyalty card):
{
key: 'loyalty_card',
name: { fr: 'Carte de fidélité', en: 'Loyalty card' },
description: { fr: 'Solde de points, palier et n° de carte du client.', en: 'Points, tier and card number.' },
targets: ['email_template', 'popup', 'smart_content'],
render_type: 'html',
render_mode: 'static',
default_width: 320,
html_template:
'<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="spm-loy"><tbody><tr>' +
'<td class="spm-loy__card"><div class="spm-loy__row">' +
'<span class="spm-loy__brand">{integration.brand_name}</span>' +
'<span class="spm-loy__tier">{var=custom_data.pos_profile.loyalty_tier}</span></div>' +
'<div class="spm-loy__pts">{var=custom_data.pos_profile.loyalty_points} <span>{widget.points_label}</span></div>' +
'<div class="spm-loy__card-no">CARTE {var=custom_data.pos_profile.loyalty_card}</div>' +
'</td></tr></tbody></table>',
css_template:
'.spm-loy{margin:0 auto}.spm-loy__card{background:{widget.bg};border-radius:14px;padding:20px 22px;color:{widget.fg}}',
config_schema: {
groups: [
{
label: { fr: 'Contenu' },
fields: [
{ key: 'points_label', type: 'text', default: 'points', label: { fr: 'Libellé points' } },
{ key: 'bg', type: 'color', default: '#3CB4A4', label: { fr: 'Couleur de fond' } },
{ key: 'fg', type: 'color', default: '#FFFFFF', label: { fr: 'Couleur du texte' } },
],
},
],
},
}Dynamic HTML widget (render_mode: "dynamic")
You reuse an existing ShopiMind renderer via renderer_key: no template, no script on your side. The declaration only carries the renderer's key and its config_schema. Real-world example (loyalty countdown):
{
key: 'loyalty_countdown',
name: { fr: 'Compte à rebours fidélité', en: 'Loyalty countdown' },
description: { fr: "Urgence : fin d'une offre fidélité / expiration des points.", en: 'Loyalty deadline countdown.' },
targets: ['email_template', 'popup', 'smart_content'],
render_type: 'html',
render_mode: 'dynamic',
renderer_key: 'spm_countdown',
default_width: 360,
config_schema: {
fields: [
{ key: 'endDate', type: 'datetime', label: { fr: "Date de fin de l'offre" } },
{ key: 'timezone', type: 'text', default: 'Europe/Paris', label: { fr: 'Fuseau' } },
{ key: 'labelDays', type: 'text', default: 'JOURS' },
{ key: 'digitBgColor', type: 'color', default: '#1a1a1a', strip_hash: true, label: { fr: 'Fond des chiffres' } },
],
},
}renderer_key must exist at ShopiMind
renderer_key: 'spm_countdown' only works because ShopiMind embeds and registers a renderer under that key. An unknown renderer_key is not rendered. You cannot ship a dynamic widget on your own for a renderer that does not yet exist — it is designed with ShopiMind.
Rendering is always on ShopiMind's side
A widget's rendering is always performed by ShopiMind, never by a call to the partner: there is no webhook or widget endpoint on the integration side — integration.widgets is pure JSON data.
image— no server-side rendering. The editor substitutes the placeholders inimage_url_templateand emits the<img>tag.html+static— rendered server-side: ShopiMind substitutes{widget.*}and{integration.*}(escaped), leaves{var=…}literal, then sanitizes the HTML and the CSS. Never any JavaScript.html+dynamic— rendered server-side by resolvingrenderer_keyin ShopiMind's renderer registry. The renderer runs inside ShopiMind's process (trusted code).
Your templates never leave the server
The public declaration exposed to the browser contains neither html_template, nor css_template, nor renderer_key. These fields are strictly server-side.
Placeholders in rendering
Four families, all resolved server-side:
| Placeholder | Source | Resolved |
|---|---|---|
{var=…} | Per-recipient ShopiMind data — e.g. {var=contact.first_name}, {var=shop.url}, {var=custom_data.<schema>.<field>} | Per recipient, at send time |
{widget.<field>} | The widget config chosen by the user (its config_schema) | At render time (escaped) |
{integration.<field>} | The non-sensitive values of the integration config | At render time (escaped) |
{LANG_…} | Translated value of a translatable field | Per recipient language, at send time |
A field enables {var=…} with supports_variables: true, and per-language translation with translatable: true. Sensitive values of the integration config are excluded from {integration.*}.
Config set by the integrator
An integration config field declared owner: "integrator" is invisible on the merchant side: it is the integrator who sets its value per shop (via the API, see Configuration). It is resolved here as {integration.<key>}, with the declared default as fallback. Handy for a tracking URL, an account identifier, etc.
A widget's config_schema
A widget's configuration panel is described by config_schema. The values chosen by the user are resolved via {widget.<field>}.
- Layout:
fields(flat) orgroups(each group = a tab) for the "Configuration" section;style_groupsfor a separate "Styles" section. - Field types:
text·number·color·select·checkbox·datetime.
Field (WidgetConfigField):
| Key | Role |
|---|---|
key | Field identifier, resolved via {widget.<key>}. |
type | text · number · color · select · checkbox · datetime. |
default | Default value. |
label | Localized label. |
options | For select: array of { value, label? }. |
supports_variables | Allows a {var=…} in the field. |
translatable | Text translated per language (resolved via {LANG_…}). |
preview_value | Sample value for the image preview when the field carries a {var=…}. |
strip_hash | type: color — emits the hex without the # (for certain image URLs). |
visible_when | Conditional visibility: { field, in: [...] }. |
refresh_fields | Re-parses the panel when this field changes (drives the visible_when). |
Takeaways
- Declare your widgets in
integration.widgets(WidgetDeclaration[]), typed against the kit. - Choose the axes:
render_type: 'image'or'html'; for HTML,render_mode: 'static'(your template) or'dynamic'(renderer_key). imageandhtml+staticship on their own — no ShopiMind code change.html+dynamicis designed with ShopiMind: only therenderer_keytravels, the logic lives at ShopiMind.- Rendering is always on ShopiMind's side, never a call to the partner; your templates never touch the browser; static is sanitized (no JS); the
{var=…}stay literal and resolve per recipient at send time. - Manual registration: your widgets travel in the neutral manifest that you hand over to ShopiMind (see → The manifest).
→ The kit in detail · Configuration · The visual studio · The manifest