# How Loops Work in Instatic for Rendering Collections

> Discover how Instatic loops render collections. Learn how base.loop expands, clones subtrees, and substitutes variables for efficient static HTML generation.

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

---

**Instatic loops work by expanding a `base.loop` node during the publish phase, cloning the child subtree for each item in a data collection and substituting variables like `$item` before static HTML generation.**

In the **CoreBunch/Instatic** repository, loops provide a declarative way to render collections without imperative JavaScript mapping. Understanding how loops work in Instatic for rendering collections reveals a compile-time expansion system that produces static HTML while maintaining dynamic data binding in the visual editor.

## The base.loop Node Structure

When editors save a page, Instatic stores a node of type **`base.loop`** in the JSON page tree. According to the TypeBox schema defined in [`src/core/visualComponents/loop/LoopSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/loop/LoopSchema.ts), this node contains three critical properties:

- **`items`**: A reference to a data collection—either a built-in system table (e.g., `posts`) or a custom query supplied by a plugin
- **`child`**: The subtree that renders once per collection element
- **`itemVar`**: The variable name injected into the render context (defaults to `$item`)

## The Expansion Pipeline

During the publish phase, the publisher walks the page tree in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts). When encountering a `base.loop` node, it executes a three-step expansion process before generating static HTML.

### Collection Resolution

The backend resolves the `items` query against the database, returning an array of plain objects. This resolution happens inside the publisher's loop handling logic.

### Subtree Cloning and Variable Substitution

For each element in the resolved array, the system clones the `child` subtree and replaces variable references (e.g., `${$item.title}`) with concrete values from the current element. The core algorithm for this transformation resides in [`src/core/publisher/loopRenderer.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/loopRenderer.ts).

### Tree Insertion

The cloned subtrees are flattened and inserted back into the page tree at the loop's original position, preserving the original order. After expansion, the tree contains only ordinary nodes like `base.text` and `base.image`, making it compatible with the rest of the rendering pipeline.

## Implementation Examples

### Defining a Loop in the Page Tree

The following JSON representation shows how the editor stores a loop node that renders the five latest posts:

```json
{
  "type": "base.loop",
  "props": {
    "items": { "query": "select * from posts order by created_at desc limit 5" },
    "itemVar": "$post"
  },
  "children": [
    {
      "type": "base.text",
      "props": {
        "content": "${$post.title}"
      }
    },
    {
      "type": "base.image",
      "props": {
        "src": "${$post.coverImage}"
      }
    }
  ]
}

```

*Source: [`src/modules/base/loop/LoopEditor.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/loop/LoopEditor.tsx)*

### Publisher Expansion Logic

The expansion logic in [`src/core/publisher/loopRenderer.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/loopRenderer.ts) handles the actual cloning and variable substitution:

```typescript
function renderLoop(node: LoopNode, ctx: RenderContext) {
  const rows = await db.query(node.props.items.query);
  const clones = rows.map(row => {
    const clone = deepClone(node.children);
    substituteVariable(clone, node.props.itemVar, row);
    return clone;
  });
  return flatten(clones);
}

```

*Source: [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts)*

### Template Syntax

Instatic also supports a markdown-like syntax that compiles to the JSON structure above:

```markdown
{% loop items="select title, thumbnail from posts limit 3" itemVar="post" %}

### {{post.title}}

![Cover]({{post.thumbnail}})
{% endloop %}

```

## Integration with Static Generation

Because loop expansion occurs *before* static HTML generation, the resulting output is fully cacheable. The LRU render cache includes data version keys in its cache keys, and any remaining dynamic fragments remain detectable by the `<instatic-hole>` logic. This architecture ensures that loops produce static markup while maintaining compatibility with Instatic's lazy-hole system for dynamic content.

## Summary

- Loops use **`base.loop`** nodes stored in the JSON page tree according to the schema in [`src/core/visualComponents/loop/LoopSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/loop/LoopSchema.ts)
- Expansion happens during publish in **[`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts)**, with core logic in **[`src/core/publisher/loopRenderer.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/loopRenderer.ts)**
- The system clones child subtrees for each collection item and substitutes variables like `$item` with concrete values
- Resulting trees contain only static nodes, making them compatible with Instatic's publishing model and LRU render cache
- Mutations to loops in the editor are handled by **[`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts)**

## Frequently Asked Questions

### What file contains the main loop expansion logic?

The main tree walking and expansion triggering occurs in **[`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts)**, while the core algorithm for resolving collections and injecting data lives in **[`src/core/publisher/loopRenderer.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/loopRenderer.ts)**.

### Can loops reference custom database queries?

Yes, the **`items`** property accepts any query string, allowing loops to reference both built-in system tables like `posts` and custom queries supplied by plugins.

### When does loop expansion occur in the rendering pipeline?

Expansion occurs during the **publish phase** before static HTML generation, ensuring the final output is static and cacheable while maintaining dynamic data binding during the build process.

### How does the visual editor handle loop mutations?

The **[`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts)** file contains the **`mutateActiveTree`** helper functions that manage loop mutations including insert, delete, and duplicate operations within the visual editor.