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 pluginchild: The subtree that renders once per collection elementitemVar: 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}}

{% 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.loopnodes stored in the JSON page tree according to the schema insrc/core/visualComponents/loop/LoopSchema.ts - Expansion happens during publish in
src/core/publisher/dynamicDetection.ts, with core logic insrc/core/publisher/loopRenderer.ts - The system clones child subtrees for each collection item and substitutes variables like
$itemwith 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
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →