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.
base.link
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
renderfunction returning{ html, css? }and registers with the global module-engine atsrc/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 insrc/core/publisher/render.tsandsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →