What Modules Are Available in Instatic and How Do They Render

Instatic ships with 15 base modules that transform JSON page trees into static HTML through a centralized registry where each module exposes a render function returning { html: string, css?: string }.

Instatic is a static site generator maintained by CoreBunch that converts authored content into optimized HTML. Understanding what modules are available in Instatic and how they render is essential for developers customizing the publishing pipeline or debugging the static output.

Layout Modules

Layout modules establish the document structure and content regions.

base.body

The base.body module in src/modules/base/body/index.ts returns a <main> element (or custom tag) that serves as the root content container. According to the source code, it emits <main data-instatic-content-region></main> and acts as the anchor point where the publisher injects the page tree.

base.container

Defined in src/modules/base/container/index.ts, this module emits semantic container elements such as <section> or <article> based on the tag prop. Children are rendered recursively inside the container’s HTML, making it the primary grouping primitive.

base.outlet

Located at src/modules/base/outlet/index.ts, the outlet module renders <${tag}${attrs}>${props.html}</${tag}> where the tag may be customized. This represents the main content entry point, and the publisher writes the final page body into this node during the rendering phase.

Typography Modules

Typography modules handle text content and list structures.

base.text

In src/modules/base/text/index.ts, the text module returns { html: <${tag}${attrs}>${text}</${tag}> } where text is escaped via textToBreakHtml. The publisher does not modify the output beyond standard sanitization, making this the most direct HTML emission path.

base.list

The base.list module at src/modules/base/list/index.ts renders <ul> or <ol> elements depending on the ordered prop. It maps each item via renderListItem(), applying the same HTML-escaping pipeline used by other modules.

Media Modules

Media modules handle images, vector graphics, and embedded video.

base.image

Found in src/modules/base/image/index.ts, this module constructs an <img> tag with responsive srcset and sizes attributes. When no image URL is present, it falls back to an SVG placeholder. Image URLs are pre-escaped by escapeProps before markup generation.

base.svg

The src/modules/base/svg/index.ts implementation returns raw SVG markup wrapped in a <svg> element with optional viewBox handling. The publisher passes this through the sanitizer unchanged for inline vector graphics.

base.video

Located at src/modules/base/video/index.ts, this module delegates to renderYoutube() which emits an <iframe> with lazy-loading attributes and CSP frame-src declarations. The publisher extracts CSP requirements from the render output at approximately line 184 of the render pipeline.

Interactive Modules

Interactive modules manage user engagement elements and data-driven repetition.

base.button

In src/modules/base/button/index.ts, the module emits either a <button> or an <a> element (if href is set) with appropriate ARIA attributes. The htmlAttributesAttr utility adds data-attributes for client-side behaviors.

The base.link module at src/modules/base/link/index.ts uses the linkUsesChildren() helper from link/content.ts to determine whether to render child nodes or the text prop:

const content = linkUsesChildren(renderedChildren.length) ? renderedChildren.join('') : props.text
return { html: `<a${attrs}>${content}</a>` }

base.loop

Defined in src/modules/base/loop/index.ts, this module returns a no-op comment {/* fallback comment */} because the publisher intercepts base.loop nodes via renderLoop() in src/core/publisher/renderLoop.ts. This allows repeating a subtree over a data source and injecting pagination or "load more" UI when needed.

Form Modules

base.forms

The forms module at src/modules/base/forms/index.ts bundles multiple sub-renderers (input, select, textarea) that each return HTML fragments. The main form renderer stitches these into a single <form> element with a hidden honeypot field for spam protection. Forms are fully static; dynamic validation is performed client-side by the editor only.

Component System Modules

These modules enable visual component composition and slot-based architecture.

base.visualComponentRef

In src/modules/base/visualComponentRef/index.ts, this module calls renderVisualComponentRef() from src/core/visualComponents/instantiate.ts. It resolves the referenced component and renders its generated HTML, serving as the bridge between the page-tree’s vc-ref nodes and actual component implementations.

base.slotInstance

Located at src/modules/base/slotInstance/index.ts, this module returns an empty { html: '', css: '' } object because slots are rendered by the vc-ref renderer. Children are rendered at the matching slotOutlet location.

base.slotOutlet

The base.slotOutlet module in src/modules/base/slotOutlet/index.ts also returns an empty node. The outlet acts as a visual placeholder in the editor only, while the real output is emitted by the visualComponentRef renderer that stitches slot fills together.

How the Rendering Pipeline Works

Module Registration and Contracts

Each module registers a ModuleDefinition with the global module-engine registry at src/core/module-engine/index.ts. The definition must expose a render function that receives validated props (via TypeBox) and returns an object with the shape { html: string, css?: string }.

Publisher Execution Flow

The publisher in src/core/publisher/render.ts walks the page tree and invokes each module’s render method, concatenating HTML fragments. For special modules, the publisher replaces the default implementation with custom interceptors:

  • base.loop: Uses renderLoop() for data-driven repetition
  • base.visualComponentRef: Uses renderVisualComponentRef() for component resolution
  • base.slotInstance and base.slotOutlet: Handled by the vc-ref renderer

Practical Rendering Examples

Registering and Rendering a Text Module

import { registry } from '@core/module-engine';
import { TextModule } from '@modules/base/text';
import { renderNode } from '@core/publisher/render';

// The module registers itself on import.
registry.registerOrReplace(TextModule);

// Example node that a page-tree might contain:
const textNode = {
  id: 'n1',
  type: 'base.text',
  props: {
    text: 'Hello, world!',
    tag: 'p',
    htmlAttributes: {}
  }
};

// Render the node – publisher will call TextModule.render internally.
const out = renderNode(textNode);
console.log(out.html);   // → <p>Hello, world!</p>

Rendering a Loop with Interceptor

import { renderLoop } from '@core/publisher/renderLoop';

// The loop node contains a `source` reference and child template.
const loopNode = { id: 'l1', type: 'base.loop', props: { mode: 'none' }, children: [...] };

const html = renderLoop(loopNode); // Returns the repeated HTML or the fallback comment.

Rendering Visual Component References

import { renderVisualComponentRef } from '@core/visualComponents/instantiate';

const vcRefNode = {
  id: 'vc1',
  type: 'base.visualComponentRef',
  props: { componentId: 'my-component', ... }
};

const { html } = renderVisualComponentRef(vcRefNode);
// The returned HTML is the fully rendered component, including any slot fills.

Summary

  • Instatic provides 15 base modules covering layout, typography, media, interactive elements, forms, and component composition.
  • Each module implements a render function returning { html, css? } and registers with the global module-engine at src/core/module-engine/index.ts.
  • Special modules like base.loop, base.visualComponentRef, and slot-related modules are intercepted by the publisher and rendered via dedicated functions in src/core/publisher/render.ts and src/core/visualComponents/instantiate.ts.
  • The publisher walks the page tree and concatenates HTML fragments, with slot composition handled by the vc-ref renderer rather than the modules themselves.
  • Forms and links include security features like honeypot fields and CSP-aware attribute handling.

Frequently Asked Questions

What is the return type of an Instatic module render function?

Every module’s render function must return an object with the shape { html: string, css?: string }. This contract is enforced by the module registry in src/core/module-engine/index.ts and consumed by the publisher pipeline to concatenate static output.

How does Instatic handle repeating content?

The base.loop module acts as a marker in the page tree but returns only a fallback comment. The publisher intercepts loop nodes via renderLoop() in src/core/publisher/renderLoop.ts, which handles the actual repetition over data sources and can inject pagination controls.

Where does slot content actually get rendered?

Slot content is rendered by the visualComponentRef renderer, not by base.slotInstance or base.slotOutlet modules themselves. These modules return empty HTML strings; the vc-ref renderer in src/core/visualComponents/instantiate.ts stitches slot fills together at the correct outlet locations.

Can I manually render a module for testing?

Yes. Import the module and the renderNode function from @core/publisher/render, ensure the module is registered with the global registry, then call renderNode(yourNode) to receive the HTML output. This pattern is used for unit testing individual modules outside the full publishing pipeline.

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 →