How Instatic Templates and Layouts Use Outlet/Insertion Patterns
Instatic implements a dual-outlet architecture where base.outlet handles page-to-layout content injection and base.slot-outlet paired with base.slot-instance manages reusable component slots, enabling nested template composition without orphaned placeholders.
Instatic's open-source static site generator employs a node-based template system that separates content structure from presentation through deterministic insertion mechanisms. The framework provides distinct outlet patterns for page-to-layout injection versus visual component slotting, each governed by specific modules in the src/core directory.
The base.outlet Pattern for Page Content Injection
Instatic enforces a single outlet per document rule for core templates. This constraint ensures that when a page renders inside a layout, the content has exactly one canonical insertion point, eliminating ambiguity regarding where body content should appear.
Detecting Outlets in Template Trees
The utility functions in src/core/templates/outlet.ts provide the canonical detection logic used during template composition. The firstOutletId(tree) function traverses the node map and returns the first node where moduleId equals 'base.outlet'. Complementary functions treeHasOutlet(tree) and subtreeHasOutlet(rootId, tree) determine outlet existence at any level of the document tree.
These helpers enable the composer to validate that a host template can accept content before attempting insertion. If no outlet exists, the system treats the template as terminal, gracefully returning the inner document unchanged rather than throwing composition errors.
Template Composition with composeTemplateChain
The composeTemplateChain function in src/core/templates/templateCompose.ts orchestrates the merge operation when nesting documents. The algorithm follows a strict replacement protocol:
- Identify the outlet ID using
firstOutletId(host)on the host template - Locate the specific node via an internal
locate(host, outletId)call - Delete the outlet node entirely using
delete merged[outletId] - Splice the inner document's nodes into the host at the outlet's former position
This outlet replacement strategy ensures the final merged tree contains only the layout's structural nodes and the page's content nodes. The base.outlet placeholder is completely removed during composition, leaving no traces in the static output.
Dynamic Content Binding
Outlets automatically receive content through the binding system defined in src/core/templates/dynamicBindings.ts. During the rendering phase, the base.outlet node's html property automatically populates with the current entry's markdown body. This implicit binding eliminates manual content assignment, allowing authors to declare an outlet once while the framework handles data injection transparently.
The Slot Outlet Pattern for Visual Components
Visual Components (VCs) utilize a different insertion mechanism based on slot outlets and slot instances. This pattern supports multiple named insertion points per component, unlike the single base.outlet restriction imposed on page templates.
Declaring Slots with base.slot-outlet
Components define available slots by including base.slot-outlet nodes in their tree structure, as declared in src/modules/base/slotOutlet/index.ts. Each outlet requires a slotName property to identify the insertion point, validated through the prop guards in src/core/visualComponents/propGuards.ts.
These outlets function as persistent placeholders that survive initial tree parsing, waiting to be filled during the instantiation phase rather than being replaced during template composition.
Slot Synchronization via slotSync.ts
When a VC reference (base.visual-component-ref) enters the canvas, the system generates matching content containers through src/core/visualComponents/slotSync.ts. The collectSlotOutlets(vcTree) function performs a depth-first pre-order traversal to gather all base.slot-outlet nodes within the component definition. For each outlet discovered, the system creates a corresponding base.slot-instance node (defined in src/modules/base/slotInstance/index.ts) as a child of the VC reference, preserving the identical slotName property.
These slot instances serve as containers for author-authored content that eventually replaces the outlets during the rendering expansion phase.
Instantiation and Outlet Expansion
The instantiateVCAtRef function in src/core/visualComponents/instantiate.ts executes the final outlet-to-content substitution:
- Apply prop overrides to the Visual Component
- Locate each
base.slot-outletnode within the component tree - Retrieve the matching
base.slot-instanceby comparingslotNamevalues - Replace the outlet's position in the tree with the instance's
childrennodes
If no slot instance exists for a given outlet (indicating an empty slot), the outlet remains as a placeholder that renders nothing. This prevents structural corruption while maintaining clear component boundaries.
Practical Outlet Implementation Examples
The following TypeScript structures demonstrate the outlet definitions used by the Instatic core:
// 1️⃣ A simple page template (has one outlet)
const pageTemplate = {
id: 'tmpl_page',
nodes: {
root: { moduleId: 'base.page', children: ['outlet'] },
outlet: { moduleId: 'base.outlet' } // ← content will be inserted here
}
};
// 2️⃣ A layout component that defines a slot
const twoColumnLayout = {
id: 'layout_two_col',
nodes: {
root: { moduleId: 'base.visual-component-ref', children: ['slotOutlet'] },
slotOutlet: { moduleId: 'base.slot-outlet', props: { slotName: 'main' } }
}
};
// 3️⃣ Editor creates a slot instance when the layout is dropped
const slotInstance = {
id: 'slotInst_1',
moduleId: 'base.slot-instance',
props: { slotName: 'main' },
children: ['userContent'] // author‑added nodes that fill the slot
};
// 4️⃣ Rendering – the outlet is replaced by the instance’s children
// (handled by src/core/visualComponents/instantiate.ts)
Publishing and Rendering Behavior
During the publishing phase, src/core/publisher/renderVisualComponentRef.ts processes any remaining base.slot-outlet nodes that survived instantiation. Empty slots render no output, producing zero HTML rather than placeholder comments or empty divs. This termination behavior ensures that unfinished templates gracefully degrade without leaving dangling artifacts in the final static files.
The system maintains this philosophy across both outlet types: base.outlet is deleted and replaced during composition, while base.slot-outlet either expands to consume its instance content or vanishes entirely during rendering.
Summary
- Single Outlet Invariant:
src/core/templates/outlet.tsenforces onebase.outletper document viafirstOutletId()andtreeHasOutlet()utilities - Destructive Composition:
composeTemplateChain()insrc/core/templates/templateCompose.tscompletely deletes the outlet node before splicing inner content into the host tree - Automatic Hydration: The dynamic bindings system auto-fills
base.outletwith markdown body content without explicit data wiring - Component Slots: Visual Components use
base.slot-outlet(placeholder) andbase.slot-instance(content container) pairs defined insrc/modules/base/, synchronized viasrc/core/visualComponents/slotSync.ts - Multi-Slot Support: Components support multiple named slots identified by unique
slotNameproperties, processed through depth-first traversal - Graceful Degradation: Empty slots render no output, and templates without outlets return content unchanged rather than throwing errors
Frequently Asked Questions
What is the difference between base.outlet and base.slot-outlet in Instatic?
base.outlet serves as the sole insertion point for page content within a layout template, automatically binding to the entry's markdown body and replaced entirely during the composeTemplateChain operation. base.slot-outlet functions as a named placeholder within Visual Components that accepts specific content blocks through corresponding base.slot-instance nodes, enabling reusable components with several customizable regions that persist through the editing lifecycle.
How does Instatic handle templates that contain no outlets?
When treeHasOutlet() returns false during composition, the system treats the host template as terminal and returns the inner document unchanged. This graceful degradation, implemented in src/core/templates/templateCompose.ts, allows unfinished or purely structural templates to exist in the hierarchy without breaking the rendering pipeline or requiring placeholder content.
Can a Visual Component define multiple slots for different content areas?
Yes. Visual Components support multiple simultaneous slots by including several base.slot-outlet nodes, each with a unique slotName property. The collectSlotOutlets() function in src/core/visualComponents/slotSync.ts discovers all slots through depth-first traversal, and instantiateVCAtRef() expands each outlet independently using its matching base.slot-instance children, supporting complex layouts with distinct header, sidebar, and footer injection points.
What happens to slot outlets that have no content assigned?
During instantiation, unpopulated slots retain their base.slot-outlet node in the tree, but the publisher in src/core/publisher/renderVisualComponentRef.ts renders them as nothing—producing no HTML output. This prevents empty placeholder artifacts in the final static files while maintaining the component's structural integrity during the editing phase.
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 →