# How to Create Custom Loop Data Sources from Plugins in Instatic

> Learn to create custom loop data sources from plugins in Instatic. Define a LoopDataSource function and register it to dynamically populate content in base.loop blocks.

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

---

**Plugins create custom loop data sources by defining a `LoopDataSource` function that returns an array of objects and registering it via `registerLoopDataSource()` during the QuickJS bootstrap phase, enabling dynamic content population in `base.loop` blocks without modifying the core static site generator.**

Instatic's plugin architecture allows developers to inject external data into the static site generation pipeline. Learning how to create custom loop data sources from plugins in Instatic enables you to render dynamic lists from APIs, databases, or computed datasets. This approach leverages the core loop registry system to resolve data at publish time while keeping the core codebase untouched.

## Understanding the Loop Architecture

The loop system in Instatic consists of three core components that work during the publishing phase. The **LoopDataSource** interface defines the contract: a function receiving request context and returning an array of plain objects or an async iterable.

According to the CoreBunch/Instatic source code, the rendering pipeline flows through [`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts), which resolves data by looking up `config.loopData?.get(node.id)`. This map is populated earlier in the process by [`server/publish/loopPrefetch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/loopPrefetch.ts), which iterates over all loop nodes and invokes the appropriate data source function from the global registry maintained in [`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts).

## Step 1: Define the Loop Data Source Function

Create a TypeScript file in your plugin that implements the **LoopDataSource** type exported from `@core/loops/types`. The function receives the current request context—containing the site, user, or query parameters—and must return an array where each element becomes a **LoopItem** accessible in templates.

### Function Signature and Context Access

The type definition in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts) specifies that your function should be async and return objects with consistent schema. Each object property becomes available via the `{{item.property}}` syntax in the Instatic editor.

```typescript
// my-plugin/src/loopData.ts
import type { LoopDataSource } from '@core/loops/types';

/** Fetches recent posts from an external API for loop rendering */
export const recentPosts: LoopDataSource = async (ctx) => {
  const resp = await fetch('https://example.com/api/posts');
  const json = await resp.json();
  // Map API response to plain objects matching your template needs
  return json.map((p: any) => ({
    id: p.id,
    title: p.title,
    summary: p.summary,
  }));
};

```

## Step 2: Register the Data Source During Bootstrap

Plugins must register their data sources during the QuickJS bootstrap phase. In your plugin's entry point—typically referenced from [`src/admin/pluginRuntimeBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pluginRuntimeBootstrap.ts)—import the `registerLoopDataSource` function from `@core/loops` and associate your function with a unique string identifier.

```typescript
// my-plugin/bootstrap.ts
import { registerLoopDataSource } from '@core/loops';
import { recentPosts } from './loopData';

// Register with a unique key used later in the editor UI
registerLoopDataSource('my-recent-posts', recentPosts);

```

The registration API in [`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts) stores this mapping in the global loop registry, making it available to the prefetcher when it encounters a loop node configured with your registered source name.

## Step 3: Configure the Loop Block in the Editor

After registration, configure the visual component to use your custom source. The `base.loop` module defined in [`src/modules/base/loop/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/loop/index.ts) exposes a **Source** property that accepts the string key you provided to `registerLoopDataSource`.

In the Instatic visual editor:

1. Add a **Loop** block (`base.loop`)
2. Set the **Source** field to `my-recent-posts`
3. Insert child elements referencing the fields your data source returns

The template syntax uses the `item` variable to access properties from your returned objects:

```html
<div class="post-card">
  <h2>{{item.title}}</h2>
  <p>{{item.summary}}</p>
</div>

```

When publishing, Instatic executes your registered function during the pre-fetch phase, caches the results in the `loopData` map, and renders each child template with the corresponding **LoopItem**.

## Core Files in the Loop Data Pipeline

The following source files implement the loop data resolution mechanism as analyzed from the CoreBunch/Instatic repository:

- **[`src/core/publisher/renderLoop.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderLoop.ts)** – The renderer that iterates over resolved data and renders child templates by looking up cached loop data via node IDs.

- **[`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts)** – Contains the TypeBox schemas for `LoopItem` and the `LoopDataSource` function signature that plugins must implement.

- **[`server/publish/loopPrefetch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/loopPrefetch.ts)** – Server-side logic that discovers all loop nodes in a page, invokes the registered data source functions, and populates the loop data cache before rendering begins.

- **[`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts)** – Exports `registerLoopDataSource()` and maintains the global mapping between source name strings and their implementing functions.

- **[`src/admin/pluginRuntimeBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pluginRuntimeBootstrap.ts)** – The entry point where plugins hook into the Instatic runtime and register custom functionality during the QuickJS bootstrap process.

- **[`src/modules/base/loop/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/loop/index.ts)** – Defines the `base.loop` block configuration, including the `source` prop that references registered loop data sources.

## Summary

Creating custom loop data sources from plugins in Instatic requires three specific implementation steps:

- **Implement the `LoopDataSource` interface** in a dedicated plugin file, ensuring the function returns an array of plain objects or an async iterable compatible with the template system.
- **Register the source** via `registerLoopDataSource()` in your plugin's QuickJS bootstrap entry point, using a unique string key that identifies the data provider.
- **Reference the registered key** in the `base.loop` block's Source property within the Instatic editor, then access item properties using `{{item.field}}` template syntax.

This architecture allows external data from APIs, databases, or custom computations to power dynamic content loops while maintaining Instatic's static site generation performance and core code isolation.

## Frequently Asked Questions

### What data format should a custom loop data source return?

A custom loop data source must return an array of plain JavaScript objects or an async iterable of objects. Each object property becomes accessible in templates via `{{item.propertyName}}`. The objects should be serializable as they are cached in the `loopData` map during the prefetch phase in [`server/publish/loopPrefetch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/loopPrefetch.ts).

### Can I access site configuration or user context inside a loop data source?

Yes. According to the type definitions in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts), the **LoopDataSource** function receives a context object as its argument. This context contains the current request state, including the site configuration, authenticated user data, and any query parameters, allowing you to filter or customize the returned dataset based on the publishing context.

### When does Instatic execute the custom data source function?

Instatic executes the function during the server-side prefetch phase before rendering begins. The [`server/publish/loopPrefetch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/loopPrefetch.ts) file discovers all loop nodes in the page, looks up the registered **LoopDataSource** in the registry from [`src/core/loops/registry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/registry.ts), invokes the function, and stores the results. This happens once at publish time, not during user page loads, ensuring static generation performance.

### Is it possible to use TypeScript generics for type-safe loop items?

While the core **LoopDataSource** type in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts) defines the base interface, you can extend it with TypeScript generics in your plugin code to ensure your returned items match your specific template schema. However, the registry system treats all sources as the base **LoopDataSource** type, so runtime validation of the shape should be handled within your data fetching logic before returning the array.