Demo Brand Demo Brand

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}}

SourceTier 1 · resolves at publish
<ul>
  {{loop var.features}}
    <li><strong>{{item.title}}</strong> — {{item.blurb}}</li>
  {{loop}}
</ul>
Rendered
  • 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

SourceTier 1 · resolves at publish
{{loop var.features}}
  <p>Step {{item.index}}: {{item.title}}</p>
{{loop}}
Rendered

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}}

SourceTier 1 · resolves at publish
{{loop var.perks}}<span class="demo-chip">{{item}}</span>{{loop}}
Rendered
Free shippingCancel anytime24/7 support

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.

SourceTier 1 · resolves at publish
<div class="demo-grid demo-grid--2">
  {{loop var.testimonials as demo-testimonial}}{{loop}}
</div>
Rendered
Alex from Berlin
I was sceptical at first — by day five it had already paid for itself.
Alex from Berlin ★★★★★
Bea from Madrid
Finally a product that does what it says on the box.
Bea from Madrid ★★★★★

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:

SourceTier 1 · resolves at publish
{{loop var.plans}}
  {{component.demo-cta kicker="{{item.name}} plan" label="Choose {{item.name}} — {{item.price}}" subtitle="Args renamed + decorated from item fields"}}
{{loop}}
Rendered

Starter plan

Choose Starter — $9/mo

Args renamed + decorated from item fields

Pro plan

Choose Pro — $29/mo

Args renamed + decorated from item fields

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.

SourceTier 1 · resolves at publish
{{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}}
Rendered

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>
SourceTier 1 · resolves at publish
{{component.demo-badge-list title="Badges from a self.* json var"}}
Rendered

Badges from a self.* json var

FastSimpleProven

Inside 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:

SourceTier 1 · resolves at publish
{{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}}
Rendered
  • 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

SourceTier 1 · resolves at publish
{{loop var.empty_list}}
  <p>You should never see this.</p>
{{loop}}
Rendered

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.