How Loops Iterate Over Data Sources with Variant Support in Instatic
Instatic loops iterate over data sources using a round-robin variant selection algorithm that cycles through child template nodes for each item while maintaining immutable per-iteration contexts to prevent state pollution.
In the CoreBunch/Instatic static site generator, loops iterate over data sources with variant support through a deterministic rendering pipeline implemented in src/core/publisher/renderLoop.ts. This architecture enables dynamic content repetition with alternating layouts while supporting both static generation at build-time and interactive infinite pagination at runtime.
Data Source Resolution and Fallback Behavior
When the publisher encounters a base.loop node, it first resolves the associated dataset through config.loopData?.get(loopId) as implemented at lines 60-66 of the renderer. This fetches the pre-computed list of items from registered providers such as collections, GraphQL queries, or custom LoopItem implementations.
If the data source is unavailable—common in editor preview environments—the system does not silently drop the layout. Instead, it emits an HTML comment to preserve the document structure and aid debugging. This ensures that empty states are visible during development rather than causing layout shifts or broken nesting.
Variant Template System and Selection Logic
The loop node’s children function as variant templates stored in a variants array. The renderer validates that at least one child exists (lines 68-71), commenting-out the loop if no templates are registered.
For each data item at index i, the renderer selects the template using modular arithmetic:
// From src/core/publisher/renderLoop.ts (lines 84-85)
const variantIndex = i % variants.length;
const selectedVariant = variants[variantIndex];
This round-robin selection means that with N child templates, the layout cycles through all N variants sequentially as items are rendered. For example, with two card variants, odd items render with the first template while even items use the second, creating visual rhythm without manual conditional logic.
Immutable Per-Iteration Context Creation
To support nested loops and component references without side effects, the system constructs an immutable context for every iteration. The implementation creates a fresh entryStack array using the spread operator:
// From src/core/publisher/renderLoop.ts (lines 89-96)
const entryStack = [...baseStack, item];
const context = new TemplateRenderDataContext(entryStack);
const iterationConfig = new RenderConfig({
...config,
templateContext: context
});
This pattern ensures that each item receives a stable, item-specific RenderConfig instance. Nested visual components or inner loops reference this isolated context without mutating the outer configuration, preventing context pollution between sibling iterations.
Rendering Execution and Pagination Support
With the variant selected and context isolated, the publisher invokes the renderNode function (lines 97-98) with the chosen variant ID and the per-iteration configuration. This produces the HTML for that specific data item using the appropriate template.
When the loop’s pagination property is set to 'infinite', the renderer adds sentinel data attributes to enable client-side hydration:
// From src/core/publisher/renderLoop.ts (lines 100-112)
attributes['data-instatic-loop'] = loopId;
attributes['data-instatic-loop-page'] = currentPage;
// Additional registration for client-side fetching...
These attributes allow the runtime JavaScript to identify paginated boundaries and fetch additional pages on demand, bridging static generation with dynamic user interactions.
HTML Wrapper Generation and Style Injection
The final output wraps the accumulated iteration HTML in a configurable container tag. The resolveHtmlTag helper (located in src/modules/base/utils/htmlTag.ts) safely selects the wrapper element, defaulting to a div when no specific tag is configured.
The system then injects the node’s classIds and inline styles through src/core/publisher/classInjection.ts (lines 14-23 and 20-27), ensuring that loop-level styling applies to the wrapper while individual variant styles handle item-specific presentation.
Practical Implementation Example
The following configuration demonstrates a blog post list with alternating card layouts:
// Loop configuration with two variant templates
{
"type": "base.loop",
"id": "posts-loop",
"props": { "pagination": "none" },
"children": [
{ "type": "base.card", "id": "card-variant-1", "props": { "style": "compact" } },
{ "type": "base.card", "id": "card-variant-2", "props": { "style": "spacious" } }
]
}
// Server-side data registration
import { registerLoopData } from '@core/loops/runtime';
registerLoopData('posts-loop', {
items: [
{ title: 'First Post', url: '/post/1' },
{ title: 'Second Post', url: '/post/2' },
{ title: 'Third Post', url: '/post/3' }
],
pageNumber: 1,
hasMore: false
});
When published, the first post renders with card-variant-1, the second with card-variant-2, and the third cycles back to card-variant-1, demonstrating the round-robin behavior.
Summary
- Data source resolution occurs via
config.loopData?.get(loopId)with HTML comment fallbacks for missing data. - Round-robin variant selection uses
i % variants.lengthto cycle through child templates deterministically. - Immutable context isolation prevents state pollution through spread-operator snapshots and fresh
RenderConfiginstances per iteration. - Infinite pagination injects
data-instatic-loopattributes for client-side runtime extension. - HTML wrapping uses
resolveHtmlTagand class injection utilities to finalize the output structure.
Frequently Asked Questions
How does Instatic handle missing loop data during rendering?
When config.loopData?.get(loopId) returns undefined, typically in editor preview environments, the renderer emits an HTML comment instead of silently omitting the layout. This preserves document structure and provides visual feedback during development.
What algorithm determines which variant template renders for each loop item?
Instatic uses round-robin selection via modular arithmetic i % variants.length, where i is the zero-based item index. This cycles sequentially through available child templates, ensuring even distribution of layouts across the dataset.
How does Instatic prevent context pollution between nested loops?
Each iteration creates an immutable snapshot using [...baseStack, item] wrapped in a new TemplateRenderDataContext and RenderConfig. This isolation ensures that nested loops or component references cannot mutate the parent loop’s state.
What attributes support infinite pagination in Instatic?
When pagination is set to 'infinite', the renderer injects data-instatic-loop, data-instatic-loop-page, and related sentinel attributes while registering the loop ID. These markers enable client-side JavaScript to fetch and append additional pages dynamically.
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 →