How Instatic Handles Visual Components and Slots: Outlet and Instance Synchronization

Instatic implements Visual Components and slots through a bidirectional synchronization system where base.slot-outlet nodes define editable regions in a component definition and base.slot-instance nodes hold the actual content, kept in perfect alignment by the syncSlotInstances pure function.

In the CoreBunch/Instatic open-source repository, Visual Components and slots form the foundation of reusable UI architecture. A Visual Component (VC) is essentially a flat node tree (NodeTree<VCNode>) that exposes specific editable "holes" called slots, allowing page authors to inject custom content without modifying the component's core structure. This system relies on two distinct module types that work in tandem to maintain a strict one-to-one relationship between slot definitions and their implementations.

The Two-Sided Slot Architecture

The slot mechanism is built on two complementary module types that separate the declaration of editable areas from the content that fills them.

base.slot-outlet acts as the declaration phase. Inside a VC’s definition tree, these nodes mark positions where content will eventually render. The slotName prop on a slot-outlet becomes the unique identifier for that slot, establishing the contract that page authors must fulfill.

base.slot-instance represents the consumption phase. When a VC is dropped onto a page, Instatic automatically materializes these nodes under the visual-component-ref node in the page tree. Each instance holds user-authored children that fill the corresponding outlet. According to the source code in src/modules/base/slotInstance/index.ts, these nodes are locked (cannot be moved or deleted) and must carry a slotName property that matches the outlet’s name exactly.

Synchronizing Slots with syncSlotInstances

The core synchronization logic lives in src/core/visualComponents/slotSync.ts within the pure function syncSlotInstances. This function guarantees that every slot-outlet declared in a VC definition has exactly one corresponding slot-instance child in the page tree, preserving the order defined by the component author.

The algorithm executes six distinct steps to reconcile the page tree with the VC definition:

  1. Collect outlet names – collectSlotOutletNames walks the VC tree and gathers the names of all slot-outlet nodes, deduplicating by name to establish the target state.
  2. Classify existing children – The ref node’s children are inspected; only nodes with moduleId === 'base.slot-instance' are kept as candidates for retention, while everything else is marked for deletion.
  3. Name-based matching – Existing slot-instances whose slotName already matches a target outlet name are retained immediately, covering cases where slots have been reordered in the definition.
  4. Positional matching – Remaining unmatched instances are paired with the remaining outlet names by their current position, generating rename operations for any slots that have been renamed rather than added or removed.
  5. Delete and insert – Unmatched instances are deleted, and any still-unmatched outlet names result in insert operations. These create new base.slot-instance nodes (with generated NanoIDs) that are locked and pre-filled with the correct slotName.
  6. Build ordered child list – The final order of children matches the order of outlet appearance in the VC tree, ensuring visual consistency.

The function returns a SyncResult containing operations, new nodes, and ordered child IDs. The companion function applySlotSyncResult applies these mutations to the mutable page-tree map and updates parent references accordingly.

Rendering and Publishing Behavior

During the publishing phase, the VC ref renderer in src/core/publisher/renderVisualComponentRef.ts extracts the children of each base.slot-instance and injects them into the matching slot-outlet positions of a synthetic VC page built from the component definition.

Notably, the base.slot-instance itself never renders any markup. Its publishBehavior is explicitly set to 'transparent' in the module definition, meaning only the outlet’s rendered output appears in the final HTML. This architectural choice ensures that the slot mechanism leaves no trace in the published output while maintaining strict editorial boundaries during the editing phase.

Practical Implementation Example

When defining a Visual Component, authors declare slots using the outlet module in the VC’s node tree:

// Defining a VC with two slots in its definition tree
import { makeNode } from '@core/utils/testing'

export const myComponent = {
  id: 'my-vc',
  name: 'MyComponent',
  tree: {
    rootNodeId: 'root',
    nodes: {
      root: makeNode('root', 'base.container', ['slotA', 'slotB']),
      // Slot outlets mark editable "holes"
      slotA: { 
        id: 'slotA', 
        moduleId: 'base.slot-outlet', 
        props: { slotName: 'header' }, 
        children: [] 
      },
      slotB: { 
        id: 'slotB', 
        moduleId: 'base.slot-outlet', 
        props: { slotName: 'footer' }, 
        children: [] 
      },
    },
  },
  params: [],
  classIds: [],
  createdAt: Date.now(),
}

When this component is instantiated on a page, syncSlotInstances automatically generates the corresponding slot instances:

// Page tree structure after synchronization
{
  // The reference node
  vcRefNode: {
    id: 'ref-1',
    moduleId: 'base.visual-component-ref',
    props: { componentId: 'my-vc' },
    children: ['inst-header', 'inst-footer'], // Ordered by outlet position
  },
  // Locked slot instances created automatically
  'inst-header': {
    id: 'inst-header',
    moduleId: 'base.slot-instance',
    props: { slotName: 'header' },
    children: [], // Authors add content here
    locked: true,
  },
  'inst-footer': {
    id: 'inst-footer',
    moduleId: 'base.slot-instance',
    props: { slotName: 'footer' },
    children: [],
    locked: true,
  },
}

Finally, the publisher renders the component by merging slot content into the outlet positions:

import { renderVisualComponentRef } from '@core/publisher'

const html = renderVisualComponentRef(vcRefNode, config, acc, renderNode)
// Output contains the VC markup with slot-instance children 
// rendered at the corresponding base.slot-outlet positions

Summary

  • Visual Components in Instatic are defined as flat node trees that use base.slot-outlet nodes to declare editable regions identified by a slotName property.
  • base.slot-instance nodes are automatically generated and locked children of a VC reference that hold the actual user-authored content for each slot.
  • The syncSlotInstances function in src/core/visualComponents/slotSync.ts maintains a one-to-one, ordered relationship between outlets and instances through a six-step reconciliation algorithm.
  • Slot instances are transparent during publishing (publishBehavior: 'transparent'), meaning only the content they carry renders in the final output, not the instance wrapper itself.

Frequently Asked Questions

What is the difference between base.slot-outlet and base.slot-instance?

The base.slot-outlet module is declared inside a Visual Component’s definition tree and marks where content should render, acting as the contract or interface. The base.slot-instance module is created automatically in the page tree when a VC is used, acting as the implementation that holds the actual user-authored content. Outlets define where content goes; instances hold what content is actually there.

How does Instatic handle slot reordering when a Visual Component definition changes?

When a VC definition is updated, syncSlotInstances reorders the page tree children to match the new outlet order. First, it matches existing instances to outlets by slotName regardless of position. Then, it rebuilds the child list to match the sequence in which outlets appear in the VC tree, ensuring the visual hierarchy remains consistent with the component author's intent.

Why are slot-instance nodes locked in the page tree?

Slot-instance nodes carry the locked: true property to prevent page authors from accidentally deleting or moving the structural containers that connect their content to the VC’s outlet positions. This lock ensures the integrity of the slot synchronization system, as these nodes are managed entirely by the syncSlotInstances algorithm rather than manual user manipulation.

What happens to slot content during the publishing phase?

During rendering, the renderVisualComponentRef function in src/core/publisher/renderVisualComponentRef.ts extracts the children from each base.slot-instance and injects them into the corresponding base.slot-outlet positions. Because slot instances have publishBehavior: 'transparent', they output no markup themselves—only the content they wrap appears in the final HTML, creating a clean semantic output.

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 →