How Loops Work in Instatic for Rendering Collections

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

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:

{
  "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

Publisher Expansion Logic

The expansion logic in src/core/publisher/loopRenderer.ts handles the actual cloning and variable substitution:

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

Template Syntax

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

{% 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

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, while the core algorithm for resolving collections and injecting data lives in 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 file contains the mutateActiveTree helper functions that manage loop mutations including insert, delete, and duplicate operations within the visual editor.

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 →