How to Create Custom Loop Data Sources from Plugins in Instatic

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, which resolves data by looking up config.loopData?.get(node.id). This map is populated earlier in the process by 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.

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

// 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—import the registerLoopDataSource function from @core/loops and associate your function with a unique string identifier.

// 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 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 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:

<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 – 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 – Contains the TypeBox schemas for LoopItem and the LoopDataSource function signature that plugins must implement.

  • 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 – Exports registerLoopDataSource() and maintains the global mapping between source name strings and their implementing functions.

  • 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 – 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.

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, 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 file discovers all loop nodes in the page, looks up the registered LoopDataSource in the registry from 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 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.

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 →