Loops
A {{loop}} block iterates a json-typed array variable and renders
its body once per element. Loops are Tier 1 only — they unroll at publish and are
stripped before the page is cached. The source arrays on this page
(features, perks, plans, testimonials) are all
editable json SITE variables: change one in the panel and this page visibly re-renders on the
next publish. A bare {{loop}} closes the nearest open loop, exactly like
{{condition}}.
Array of objects — {{item.field}}
<ul>
{{loop var.features}}
<li><strong>{{item.title}}</strong> — {{item.blurb}}</li>
{{loop}}
</ul>- Static-first — HTML is pre-rendered at publish and served from local disk.
- Two-tier engine — Variables collapse at publish; visitor tags collapse per request.
- Shared components — Reusable blocks update live across every site that uses them.
Each element is a JSON object; {{item.field}} is the current element's field. A missing field, or one whose value is a nested array/object, renders empty. Values are HTML-escaped, so a field containing < or " is encoded rather than injected.
{{item.index}} — the 0-based position
{{loop var.features}}
<p>Step {{item.index}}: {{item.title}}</p>
{{loop}}Step 0: Static-first
Step 1: Two-tier engine
Step 2: Shared components
index is a reserved name on item.* — it is the element's 0-based position, not a data field.
Array of scalars — bare {{item}}
{{loop var.perks}}<span class="demo-chip">{{item}}</span>{{loop}}When the array holds plain strings/numbers, bare {{item}} is the element itself.
Implicit component expansion — as
{{loop var.X as Component}} emits one component tag per element,
auto-binding every scalar element field as a same-named inline arg. The
demo-testimonial schema (author, quote, avatar_url, rating) matches the
testimonials elements 1:1, which is exactly when this form shines. Because the
auto-bound fields are inline args, they sit at the top of the component precedence chain and
beat any panel row or schema default of the same name.
<div class="demo-grid demo-grid--2">
{{loop var.testimonials as demo-testimonial}}{{loop}}
</div>I was sceptical at first — by day five it had already paid for itself.
Finally a product that does what it says on the box.
Explicit component args — rename, subset, decorate
Write the component tag yourself inside the loop body and pass {{item.field}}
into whichever args you want — renaming fields, passing a subset, or mixing in literal text:
{{loop var.plans}}
{{component.demo-cta kicker="{{item.name}} plan" label="Choose {{item.name}} — {{item.price}}" subtitle="Args renamed + decorated from item fields"}}
{{loop}}Loops unroll before output substitution and component expansion, so the emitted component tags are picked up by the normal component pass.
Nested loops — nearest loop wins
Each loop owns its own item.* scope; inside the inner block, item is the
inner element. An inner loop's source is resolved independently against
var.*/global.* — you cannot loop item.something.
{{loop var.plans}}
<div class="demo-plan">
<h4>{{item.name}} <small>(plan #{{item.index}})</small></h4>
<ul>
{{loop var.perks}}<li>{{item}}</li>{{loop}}
</ul>
</div>
{{loop}}Starter (plan #0)
- Free shipping
- Cancel anytime
- 24/7 support
Pro (plan #1)
- Free shipping
- Cancel anytime
- 24/7 support
A loop inside a component body
Inside a component body the only legal loop source is a self.* json variable — a body reads page-context data exclusively through its self vars. This is the component's body:
<!-- component: demo-badge-list -->
<div class="demo-badges">
<p class="demo-badges__title">{{self.title}}</p>
{{loop self.badges}}<span class="demo-chip">{{item}}</span>{{loop}}
</div>{{component.demo-badge-list title="Badges from a self.* json var"}}Badges from a self.* json var
FastSimpleProvenInside a body the passes run: condition self.* → self.* output tags → loop self.* → nested components. The badges come from the badges schema default; override them per-site in the component panel.
Loop wrapped in a condition
Conditions collapse before loops unroll — a false condition strips the loop before any iteration:
{{condition var.show_promo}}
<ul>
{{loop var.perks}}<li>{{item}}</li>{{loop}}
</ul>
{{else}}
<p>Promo off — the loop above was stripped without iterating.</p>
{{condition}}- Free shipping
- Cancel anytime
- 24/7 support
Flip the boolean show_promo SITE variable to watch the whole loop disappear.
Empty array drops the block
{{loop var.empty_list}}
<p>You should never see this.</p>
{{loop}}An empty, missing, or malformed array (e.g. a JSON object instead of a list) drops the whole block — the same behaviour as a false condition. The rendered pane is intentionally empty. Decoding never throws.