Templates
This guide covers the technical details of how Allegro templates are rendered on the page. For an overview of creating and managing templates, see Templates in the product docs.
If you use Claude Code, install the allegro-templates skill for in-editor
guidance on file structure, Alpine.js patterns, field placeholders, and local
preview:
npx skills add alleyinteractive/allegro@allegro-templates
Rendering Model
When an Interaction triggers, Allegro creates a host <div> and attaches an
open shadow root to it. All of the template's markup, styles, and external
stylesheets are injected inside that shadow root before the host is placed in
the DOM at the configured target position.
The shadow DOM provides full CSS isolation — styles defined in the template don't leak out to the host page, and the host page's styles don't bleed in.
The host element carries a data-allegro-interaction attribute set to the
Interaction's slug, which you can use for external targeting if needed:
/* from the host page — selects the template host element */
[data-allegro-interaction='my-interaction'] {
margin-top: 2rem;
}
Alpine.js
Alpine.js v3 is loaded automatically the first time a
template renders — no <script> tag required. It is served from your Allegro
domain, not a third-party CDN, so no external request leaves the page. If Alpine
is already present on the page, Allegro uses it directly.
Alpine is initialized on the shadow root, so directives in your template HTML work as expected.
x-data Scope
Every template's root container is wired up with the allegroInteraction
Alpine component, which gives you reactive access to the
current session state and any data passed to the Interaction trigger.
| Property | Type | Description |
|---|---|---|
slug | string | null | The slug of the Interaction that triggered this template. |
session | object | null | Reactive shortcut to $store.allegro.session — updates automatically on login or logout without a page reload. |
audienceMember | object | null | Reactive shortcut to $store.allegro.audienceMember — the authenticated member's profile, or null if not logged in. |
isAuthenticated | boolean | Reactive shortcut to $store.allegro.isAuthenticated. |
plans | array | The offer's plans, for a template that supports plans. See Plans. |
| (extra data) | any | Any additional data passed when the Interaction was triggered is available at the top level. |
Example — personalizing content based on authentication state:
<div x-show="isAuthenticated">
Welcome back, <span x-text="audienceMember.name"></span>!
</div>
<div x-show="!isAuthenticated">
<p>Sign in to continue reading.</p>
<allegro-login-form></allegro-login-form>
</div>
Example — reading extra data passed by the Interaction trigger:
<!-- if the interaction was triggered with { articleId: '123' } -->
<p x-text="`Reading article ${articleId}`"></p>
The session state is reactive — if the member logs in after the template is already on the page, Alpine re-renders any bound expressions automatically. See Alpine State Reference for the full store shape, the reactivity model, and more recipes.
For the full list of Alpine directives and features, see the Alpine.js documentation.
Web Components
Allegro's built-in web components are registered on the page by the SDK and are available inside templates:
| Component | Description |
|---|---|
<allegro-login-form> | Login form with email/password and social login |
<allegro-email-form> | Email capture form |
<allegro-content-gate> | Paywall / content gate |
See the Components section for each component's attributes, events, and CSS variables.
Field Placeholders
Field values defined on the Interaction are substituted into the template HTML
before rendering. Reference a field by its slug wrapped in {% %}. Values are
inserted as text, with <, >, &, and quotes escaped, except for a rich text
field, whose cleaned HTML is inserted as-is:
<h2>{% headline %}</h2>
<p>{% bodyText %}</p>
<a href="{% ctaUrl %}">{% ctaLabel %}</a>
Substitution runs before Alpine.js initializes, so field values are baked into the HTML and can be used inside Alpine expressions:
<div x-data="{ open: false }">
<button @click="open = !open">{% toggleLabel %}</button>
<p x-show="open">{% bodyText %}</p>
</div>
Repeatable Fields
A repeatable field holds a list of values. Repeat part of the HTML once per item
with {% for item in slug %}…{% endfor %}, where item is any name you choose
for the current item:
<ul>
{% for link in links %}
<li><a href="{% link %}">{% link %}</a></li>
{% endfor %}
</ul>
For a group, each item has one value per subfield. Reference a subfield with
{% item.subslug %}:
{% for newsletter in newsletters %}
<label>
<input type="checkbox" value="{% newsletter.id %}" />
<strong>{% newsletter.title %}</strong>
<img src="{% newsletter.image %}" alt="" />
</label>
{% endfor %}
A group's subfield can be a list or a group itself. Loop over it from inside the
item with {% for entry in item.subslug %}:
{% for section in sections %}
<h3>{% section.heading %}</h3>
<dl>
{% for q in section.questions %}
<dt>{% q.question %}</dt>
<dd>{% q.answer %}</dd>
{% endfor %}
</dl>
{% endfor %}
Other fields can be used inside a loop as usual. A few rules:
- Loops nest at most three levels, matching how deep groups can go.
- A loop variable with the same name as a field hides that field inside the loop.
- A loop over a field with no items renders nothing.
- An item's empty or missing subfield renders as an empty string.
{% slug %}for a repeatable field (outside a loop) renders as an empty string.
Meter Placeholders
{% meter.<slug>.<field> %} renders the visitor's usage for a meter, where
<field> is used, limit, remaining, status, or counted. Meter values
are substituted after field values, so a field value can contain a meter
placeholder too. An unknown meter or an empty value renders as an empty string.
<p x-show="'{% meter.monthly-articles.status %}' === 'under_limit'">
{% meter.monthly-articles.remaining %} free articles left
</p>
See Meters for what each field means.
Plans
A template that declares "supports": ["plans"] in its
template.json shows the plans of an offer. Each
Interaction that uses the template chooses the offer, and the template receives
that offer's plans in the offer's order. If the Interaction has no offer, or the
offer has no plans, the template is not rendered at all.
In HTML
Repeat part of the HTML once per plan with
{% for plan in plans %}…{% endfor %}. plan is any name you choose:
<ul class="plans">
{% for plan in plans %}
<li class="plan">
<span class="plan__badge">{% plan.fields.badge %}</span>
<h3>{% plan.name %}</h3>
<p>{% plan.formatted_price %} / {% plan.duration_label %}</p>
<p>{% plan.description %}</p>
<button type="button" data-plan="{% plan.id %}">
Choose {% plan.name %}
</button>
</li>
{% endfor %}
</ul>
| Placeholder | Value |
|---|---|
{% plan.id %} | The plan's ID, for allegro.purchase.initiate(). |
{% plan.name %} | The plan's name. |
{% plan.description %} | The plan's description. |
{% plan.price %} | The price in the currency's smallest unit, e.g. 9900. |
{% plan.currency %} | Lowercase ISO 4217 code, e.g. usd. |
{% plan.formatted_price %} | The price formatted for its currency, e.g. $99.00. |
{% plan.duration_label %} | How long the plan lasts, e.g. 1 year. |
{% plan.is_unlimited %} | true or false. |
{% plan.product.<key> %} | The plan's product: id, name, or slug. |
{% plan.fields.<slug> %} | A public plan field value. |
Plan values are inserted as text: <, >, &, and quotes are escaped, so a
plan named <b>Annual</b> shows the tags rather than bold text. Numbers and
booleans render as text, and an empty value, a private plan field, or an unknown
key renders as an empty string. Plan placeholders only work inside the loop, and
the plans loop can't be nested inside another loop or contain one.
Plan placeholders are text substitutions. Don't put them inside Alpine
expressions or other attributes evaluated as JavaScript, such as x-show or
@click; use the Alpine plans data there instead.
Templates embedded with <allegro-template> don't
receive plans. Put the plans loop in the template the Interaction uses.
In Alpine.js
The same list is available as plans in the template's
x-data scope, with numbers and booleans kept as their real
types:
<template x-for="plan in plans" :key="plan.id">
<button type="button" @click="allegro.purchase.initiate(plan.id)">
<span x-text="plan.name"></span>
<span x-show="plan.fields.badge" x-text="plan.fields.badge"></span>
</button>
</template>
In index.js
For a template that supports plans, index.js runs with plans in scope
alongside shadowRoot and hostElement. Don't declare your own plans
variable in such a script. plans is not defined for templates that don't
support plans, so their scripts are unaffected.
shadowRoot.querySelectorAll('[data-plan]').forEach((button) => {
button.addEventListener('click', () =>
allegro.purchase.initiate(button.dataset.plan),
);
});
console.log(`${plans.length} plans`);
Checkout is up to the template: Allegro renders the plans but adds no buttons or
click handlers. Start a checkout with
allegro.purchase.initiate(planId).
Each plan has this shape:
{
"id": "9f1c2d3e-…",
"name": "Annual",
"description": "Best value",
"price": 9900,
"currency": "usd",
"formatted_price": "$99.00",
"duration_label": "1 year",
"is_unlimited": false,
"product": {
"id": "…",
"name": "Digital Access",
"slug": "digital-access"
},
"fields": { "badge": "Most popular", "ribbon": null }
}
product is null when the plan has no product. fields holds every public
plan field, with null for one the plan has no value for.
Embedding Other Templates
A template can embed another template inline with the <allegro-template>
element. Reference the template you want to embed by its slug with the ref
attribute:
<allegro-template ref="newsletter-cta"></allegro-template>
At render time the element is replaced by the referenced template's markup. The embedded template's styles are injected into the same shadow root (once, even if you embed it multiple times), and any script it carries runs after its markup is inlined.
Passing Field Values
Any attribute other than the reserved ref and eager is passed to the
embedded template as a field value, filling its {% %}
placeholders by slug:
<allegro-template
ref="newsletter-cta"
headline="Subscribe today"
ctaLabel="Sign up"
></allegro-template>
Placeholders the parent does not supply are left blank, and loops over them render nothing.
To pass a repeatable field, write its items as a JSON array in single quotes:
<allegro-template
ref="link-list"
links='["https://acme.com/news", "https://acme.com/sports"]'
></allegro-template>
Lazy vs. Eager Loading
By default an embedded template is lazy — it loads and expands only when it
scrolls into the viewport. Add the eager attribute to expand it immediately as
the parent renders:
<allegro-template ref="hero" eager></allegro-template>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
ref | text | — | Slug of the template to embed. Required. |
eager | boolean | false | Expand immediately on render instead of waiting until the element scrolls into view. |
| (any other) | text | — | Passed to the embedded template as a field value, substituting its matching {% slug %} placeholder. A JSON array fills a repeatable field. |
Embedded templates may themselves embed others, up to a depth of 10 levels. A
missing or unknown ref, or a cyclic reference (a template that embeds itself,
directly or indirectly), is skipped — the element is removed and a warning is
logged to the console.
Display Tracking
When an Interaction offers a template, the SDK automatically records whether that template was actually seen by the visitor and emits one of two events. You don't need to wire anything up — these fire on their own.
| Event | When it fires |
|---|---|
template_displayed | The first time any part of the rendered template scrolls into the viewport. |
template_not_displayed | When the visitor leaves the page having never seen an offered template — including when a scroll delay was never reached, so the template never rendered. |
Each template offered on the page reports at most one of these events per page
load. Both events carry the template's ID as object_id and identify the
Interaction in the event data:
| Field | Description |
|---|---|
object_type | Always template. |
object_id | The ID of the template that was offered. |
data.interaction_slug | The slug of the Interaction that offered the template. |
data.template_name | The name of the template that was offered, or null if it has no title. |
data.action_index | The position of the template's action within the Interaction. |
data.interaction_id | The ID of the Interaction, when it was matched by automatic targeting. Omitted for manual triggers. |
data.journey_id | The ID of the journey that matched, or null for Everyone else. Omitted for manual triggers. |
Display tracking is suppressed in preview and token-redirect flows (where delays are skipped), so previewing a template does not generate display events.
See the Event Tracking guide for how events are sent and the fields each one carries.