# How Instatic Loop Blocks Render Dynamic Content from Data Tables

> Learn how Instatic loop blocks render dynamic content from data tables by iterating over collections prefetching data and rendering child templates for each item

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-08-02

---

**Instatic loop blocks iterate over collections of LoopItem objects by registering a LoopEntitySource, prefetching data during the publish run, and rendering child templates for each item with isolated context stacks.**

In the CoreBunch/Instatic codebase, loop blocks serve as the primary mechanism for transforming static templates into data-driven experiences. Understanding how these blocks resolve and render **dynamic content from data tables** requires examining the three-stage pipeline that connects source registration to iterative HTML generation.

## Stage 1: Registering LoopEntitySource Definitions

Every loop block relies on a registered **LoopEntitySource** that defines where items originate and what fields they expose. Sources are registered through the plugin SDK and stored in a singleton registry located at [`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts).

The registration process uses the `registerSource` function to declare metadata including filter schemas, sort options, and the asynchronous fetch implementation:

```typescript
// src/core/loops/registry.ts (usage pattern)
import { registerSource } from '@core/loops/registry';

registerSource({
  id: 'data.rows',
  label: 'Data Table Rows',
  requestDependent: false,
  filterSchema: { /* field definitions */ },
  orderByOptions: [{ id: 'createdAt', label: 'Created' }],
  fields: [{ id: 'title', label: 'Title', format: 'plain' }],
  async fetch(ctx) {
    // Returns a LoopFetchResult containing items and totalItems
    return { items: [], totalItems: 0 };
  }
});

```

Each source implementation must conform to the contracts defined in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts), ensuring consistent handling of **LoopItem** objects across the rendering pipeline.

## Stage 2: Prefetching and Data Resolution

During a publish run, the server walks the page tree and prefetches data for loops using pre-fetchable sources. This logic resides in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts), which populates `RenderConfig.loopData` with a `Map<string, ResolvedLoopRenderData>` keyed by the loop node ID.

When the publisher encounters a `base.loop` node, it invokes `resolveLoopData()` from [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts). This function determines whether to use pre-fetched data or resolve a field-based source dynamically by reading the current entry-stack:

- **Pre-fetched sources**: Data is retrieved from the `RenderConfig.loopData` map using the node's unique identifier.
- **Field-based sources**: The system resolves filters by accessing the current context stack, allowing loops to derive data from parent entry fields.

## Stage 3: Iterative Rendering with Context Isolation

The core rendering logic in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) transforms resolved data into HTML through a strict iteration protocol:

1. **Data retrieval**: Obtain the `LoopFetchResult` containing the `items` array and `totalItems` count.
2. **Context stacking**: For each item, build a fresh `templateContext.entryStack` using the spread operator (`[...baseStack, item]`) to ensure inner bindings resolve against the current item only.
3. **Variant cycling**: Select child templates using modulo arithmetic (`variantId = children[i % children.length]`), enabling alternating layouts within the same loop.
4. **Delegation**: Render each child by calling the internal `renderNode` function with the isolated context.

Pagination is handled natively within this file. When `pagination='infinite'` is configured, the renderer injects sentinel attributes like `data-instatic-loop-id` and records the loop ID in `acc.infiniteLoopIds`. This allows the runtime script to fetch subsequent pages client-side via the `/_instatic/loop/<loopId>?page=N` endpoint.

The final output is wrapped in an HTML tag determined by `resolveHtmlTag` (defaulting to `<div>`), with class IDs and inline styles injected via `injectNodeClassIds` and `injectNodeInlineStyles`. If no data resolves, the publisher emits an HTML comment for diagnostic purposes.

## Dynamic vs. Static Loop Classification

Not all loops are baked into the static Layer A artifact. The detection logic in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) classifies loops as **dynamic** when their source sets `requestDependent: true` or `perVisitor: true`.

Dynamic loops bypass static generation and render on-demand in Layer C holes. These responses can be cached in Layer B based on request parameters, while Layer A remains a static shell. Static loops, conversely, have their entire HTML output generated at publish time.

## Practical Implementation Examples

### Registering a Custom External Source

To pull data from a third-party API, implement the `fetch` method with `requestDependent: true` for dynamic rendering:

```typescript
import { registerSource } from '@core/loops/registry';
import type { SourceFetchContext, LoopFetchResult } from '@core/loops/types';

registerSource({
  id: 'acme.products',
  label: 'Acme Product Catalog',
  requestDependent: true,  // Forces Layer C rendering
  filterSchema: { /* UI filters */ },
  orderByOptions: [{ id: 'price', label: 'Price' }],
  fields: [
    { id: 'name', label: 'Product Name', format: 'plain' },
    { id: 'image', label: 'Image', format: 'media' },
    { id: 'price', label: 'Price', format: 'plain' }
  ],
  async fetch(ctx: SourceFetchContext): Promise<LoopFetchResult> {
    const { filters, limit, offset } = ctx;
    const resp = await fetch(`https://api.acme.com/products?${new URLSearchParams(filters)}`);
    const data = await resp.json();
    
    const items = data.products.slice(offset, offset + limit).map(p => ({
      id: p.id,
      fields: {
        name: p.title,
        image: p.mediaUrl,
        price: `$${p.price}`
      }
    }));
    
    return { items, totalItems: data.total };
  },
  preview() {
    // Return static data for the editor canvas
    return [{ id: 'demo', fields: { name: 'Demo', image: '/demo.png', price: '$0' } }];
  }
});

```

### Loop Module Configuration

The editor UI defines loop blocks in [`src/modules/base/loop/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/loop/index.ts) using a structured schema:

```typescript
// src/modules/base/loop/index.ts
export const LoopModule = {
  name: 'Loop',
  propsSchema: {
    sourceId: { type: 'string' },
    filters: { type: 'object' },
    pagination: { enum: ['none', 'infinite'] },
    tag: { type: 'string' },
    customTag: { type: 'string' }
  },
  defaults: {
    pagination: 'none',
    tag: 'div'
  }
};

```

### Resulting HTML Structure

A rendered loop with infinite pagination and multiple child variants produces markup with data attributes for runtime hydration:

```html
<div data-instatic-loop="loop-123" 
     data-instatic-loop-page="1" 
     data-instatic-loop-mode="infinite"
     data-instatic-loop-has-more="true" 
     data-instatic-loop-page-size="10">
  <!-- Child variant 0 -->
  <section class="card">Content bound to item 0</section>
  <!-- Child variant 1 -->
  <section class="card">Content bound to item 1</section>
  <!-- Child variant 0 (cycled) -->
  <section class="card">Content bound to item 2</section>
</div>

```

## Summary

- **LoopEntitySource** objects define data origins and are registered in [`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts) via `registerSource`.
- The publisher prefetches static loop data during the render configuration phase in [`src/core/publisher/renderConfig.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderConfig.ts).
- `resolveLoopData()` in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) handles both pre-fetched and field-based data resolution.
- Each iteration creates an isolated `entryStack` to prevent context leakage between items.
- Dynamic loops marked with `requestDependent: true` are detected by [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) and rendered in Layer C rather than static Layer A.
- Infinite pagination injects sentinel attributes and loop IDs to enable client-side page fetching.

## Frequently Asked Questions

### What is the difference between a LoopEntitySource and a LoopItem?

A **LoopEntitySource** is the configuration object that defines how to fetch and structure data (including filters, sort options, and the `fetch` method), while a **LoopItem** represents a single row or record returned by that source's fetch implementation. The source provides the blueprint; items are the instantiated data objects rendered by the loop block.

### How does pagination work in Instatic loop blocks?

When `pagination='infinite'` is set in the loop properties, [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) injects data attributes like `data-instatic-loop-id` and records the loop identifier in `acc.infiniteLoopIds`. The runtime JavaScript uses these markers to call the `/_instatic/loop/<loopId>?page=N` endpoint, fetching additional items and appending them to the DOM without reloading the page.

### When should a source set `requestDependent` to true?

Set `requestDependent: true` (or `perVisitor: true`) when the loop data varies by request context, such as personalized user content, geolocation-specific data, or real-time API results. According to [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), this classification moves the loop from static Layer A generation to dynamic Layer C rendering, executing the fetch logic on each request rather than at build time.

### How does the publisher handle empty loop data?

When `resolveLoopData()` returns no items, the rendering pipeline in [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts) emits an HTML comment instead of generating wrapper tags or child elements. This provides a diagnostic marker in the output while keeping the DOM clean, and prevents styling issues that would occur with empty container elements.