# How Loops Render Collections with Variant Support in Instatic

> Learn how Instatic's renderLoop publisher iterates collections and renders deterministic variants for each item, ensuring a stateless and type-safe pipeline. Explore LoopItem arrays and round-robin rendering for efficient colle...

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-28

---

**TLDR:** In Instatic, the `renderLoop()` publisher function iterates over pre-fetched `LoopItem` arrays and cycles through a loop's child nodes via a round-robin algorithm, rendering a deterministic variant for every item while keeping the pipeline stateless and type-safe.

The Instatic static-site engine treats `base.loop` as the core primitive for repeating template markup across collections. According to the CoreBunch/Instatic source code, the publisher pipeline fetches data upfront, resolves variants at render time, and wraps each iteration in a fresh `RenderConfig` snapshot. This architecture is how loops render collections with variant support in Instatic while remaining fully stateless and type-safe.

## The Instatic Loop Rendering Pipeline

### Data Source Registration and Pre-Fetching

Plugins and built-ins register loop data sources through `api.cms.loops.registerSource`, returning a `LoopEntitySource` typed in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts). Before publishing begins, `prefetchLoopData()` collects items for every loop ID and stores them in `RenderAccumulators.loopData` as a `Map<string, ResolvedLoopRenderData>`. This map is the single source of truth that `renderLoop()` consults during rendering.

### The renderLoop() Entry Point

The core renderer is `renderLoop()` in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts). It accepts the loop node, the global `RenderConfig`, the accumulator object, and a `renderNode` callback. If no data exists for the loop, the publisher emits an HTML comment so the missing binding is visible in the output: `<!-- instatic: loop "…" has no resolved data -->`.

## Variant Selection and the RenderLoop Algorithm

### Round-Robin Variant Cycling

A loop node may contain multiple child nodes, each representing a distinct template variant. The renderer builds a variant list with `const variants = node.children ?? []`. For every item at index `i`, the selected child is determined by `variantId = variants[i % variants.length]`. This modulo arithmetic assigns each iteration a deterministic layout: two children produce an alternating rhythm, three children cycle through a trio, and any larger set follows the same pattern.

### Per-Iteration RenderConfig Snapshots

For every iteration, the publisher creates a fresh `RenderConfig` whose `templateContext.entryStack` is a new array containing the base stack plus the current `LoopItem`. This immutable snapshot guarantees that nested visual-component references or nested loops see the correct item-specific data without mutating a shared stack.

## Pagination, Wrappers, and Style Injection

### Infinite Scroll Support

When `props.pagination === 'infinite'`, `renderLoop()` adds sentinel attributes such as `data-instatic-loop-mode="infinite"` to the wrapper and records the loop ID in `acc.infiniteLoopIds`. A client-side runtime then fetches subsequent pages from `/_instatic/loop/<loopId>?page=N`.

### HTML Tag Resolution and Styling

The loop's outer wrapper tag is resolved through the shared `resolveHtmlTag` helper and defaults to `div`. After rendering the body, the publisher injects the node’s class IDs via `injectNodeClassIds`, inline styles via `injectNodeInlineStyles`, and, if annotation is enabled, the node ID via `injectNodeId`.

## Implementing Loops with Variant Support in Instatic

The following examples show how to define a loop with multiple variants, register a data source, configure module props, and inspect the rendered output.

```tsx
// ── Define a loop with two variant children ─────────────────────────
// In the visual editor, add a `base.loop` module and nest two containers:
//   <Container> … layout A … </Container>
//   <Container> … layout B … </Container>

```

```typescript
// ── Register a data source (server-side) ────────────────────────────
// src/core/plugin-sdk/types/loops.ts – plugin example
import { api } from '@core/plugin-sdk';

api.cms.loops.registerSource({
  id: 'example.posts',
  fetch: async ({ db }) => {
    const rows = await db.query(`SELECT * FROM posts`);
    return { items: rows.map(row => ({
      fields: { title: row.title, body: row.body },
    })) };
  },
});

```

```json
{
  "sourceId": "example.posts",
  "limit": 6,
  "pagination": "none",
  "tag": "section"
}

```

```html
<!-- Rendered output (excerpt) for 6 posts -->
<section data-instatic-loop="loop-abc123">
  <div class="variant-0"> … layout A for post 0 … </div>
  <div class="variant-1"> … layout B for post 1 … </div>
  <div class="variant-0"> … layout A for post 2 … </div>
  <div class="variant-1"> … layout B for post 3 … </div>
  <div class="variant-0"> … layout A for post 4 … </div>
  <div class="variant-1"> … layout B for post 5 … </div>
</section>

```

## Summary

- **Pre-fetching:** `prefetchLoopData()` stores resolved loop items in `RenderAccumulators.loopData` before rendering starts.
- **Variant cycling:** `renderLoop()` in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) uses `variants[i % variants.length]` to assign a deterministic child template to each item.
- **Stateless snapshots:** Each iteration receives a fresh `RenderConfig` with an updated `templateContext.entryStack` to avoid shared-state bugs.
- **Pagination hooks:** Infinite loops register sentinel attributes and endpoint patterns for client-side page fetching.
- **Styling pipeline:** The wrapper tag, class IDs, inline styles, and node IDs are injected after the loop body is generated.

## Frequently Asked Questions

### What is the primary function of renderLoop() in Instatic?

`renderLoop()` in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) is the core publisher function that iterates over a loop's pre-fetched data items, selects the appropriate variant child for each iteration, and returns the rendered markup. It also handles empty states by emitting diagnostic HTML comments and tracks infinite-scroll loops in the render accumulator.

### How does Instatic determine which variant to render for each loop item?

The renderer collects child nodes into `const variants = node.children ?? []` and computes the variant index with `variantId = variants[i % variants.length]`. This round-robin algorithm cycles through the available child templates based on the item's zero-based position in the data array.

### Can loops in Instatic support more than two variants?

Yes. Because variant selection relies on modulo arithmetic, a loop can contain any number of child nodes. Two children create an alternating layout, three children create a tri-cycle, and larger sets follow the same deterministic pattern without additional configuration.

### How does pagination work with loop rendering in Instatic?

When a loop's `props.pagination` is set to `'infinite'`, `renderLoop()` adds `data-instatic-loop-mode="infinite"` to the wrapper element and records the loop ID in `acc.infiniteLoopIds`. The runtime later requests extra pages from `/_instatic/loop/<loopId>?page=N` to append new items to the DOM.