How Loops Render Collections with Variant Support in Instatic

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

// ── 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>
// ── 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 },
    })) };
  },
});
{
  "sourceId": "example.posts",
  "limit": 6,
  "pagination": "none",
  "tag": "section"
}
<!-- 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →