How Visual Components and Slots Work in Instatic's Page Tree
Visual Components in Instatic expose named slots via base.slot-outlet declarations, while the page tree automatically materializes locked base.slot-instance children to hold consumer content, kept in sync by the syncSlotInstances algorithm and rendered during publishing by matching slot names to their corresponding outlets.
Instatic's page tree architecture treats Visual Components and slots as first-class primitives for building reusable page elements. When a content author drops a Visual Component onto a page, the system instantiates a base.visual-component-ref node that dynamically generates locked slot containers based on the component's internal slot definitions. This article examines the source code in src/core/visualComponents/ to explain the synchronization logic, recursion guards, and runtime rendering flow that make this system work.
Core Architecture: Visual Components and Slots
The relationship between a Visual Component and its slots involves three distinct node types living in different parts of the tree.
The Component Reference Node (base.visual-component-ref)
A base.visual-component-ref node lives on the consumer's page and references a Visual Component definition by its componentId. It may also specify propOverrides to customize the component's behavior. This node acts as the anchor point where slot synchronization occurs.
Slot Declarations (base.slot-outlet)
Inside the Visual Component definition itself, developers place base.slot-outlet nodes to mark named positions where consumer content should be injected. Each outlet declares a slotName property (defaulting to "children" if omitted) that links it to its corresponding instance during publishing.
Slot Containers (base.slot-instance)
When a Visual Component reference is created or updated, Instatic automatically generates base.slot-instance nodes as locked children of the reference node. These instances hold the actual content provided by the page author and are synchronized to match the component's outlet definitions. Because they are locked, the UI prevents renaming, moving, or deleting these nodes directly, ensuring structural integrity.
Synchronizing Slot Instances with Component Definitions
The synchronization engine lives in src/core/visualComponents/slotSync.ts and ensures that the children of a base.visual-component-ref always match the current slot definitions of the referenced component.
Collecting Outlet Names
Before synchronizing, the system calls collectSlotOutletNames to walk the component definition tree and extract the set of declared slot names:
export function collectSlotOutletNames(tree: { nodes: NodeMap; rootId: string }): Set<string>
This function scans the component's node map for all base.slot-outlet modules and returns a Set<string> containing the valid slot identifiers.
The Sync Algorithm
The syncSlotInstances function (lines 158-236 in slotSync.ts) performs a four-phase diff operation to produce a SyncResult describing three possible operations:
- Insert – Schedule creation of a new locked
base.slot-instancefor a slot that exists in the component definition but lacks a corresponding instance on the reference. - Rename – Update the
props.slotNameof an existing instance when the slot's name has changed in the component definition but the content should be preserved. - Delete – Remove an instance and its entire subtree when the corresponding slot no longer exists in the component.
The algorithm first maps existing slot-instances by their current slotName, then identifies matches requiring renames, schedules deletions for orphaned instances, and finally schedules insertions for new slots.
Applying Sync Results Atomically
Once the diff is computed, applySlotSyncResult (lines 267-313) mutates the page's node map in a specific order: rename → delete → insert. After applying these operations, it re-orders the parent reference node's children array to reflect the slot order defined in the component, ensuring visual consistency.
Guarding Against Recursive Components
Visual Components may reference other Visual Components, creating potential for infinite recursion. The src/core/visualComponents/recursionGuard.ts module prevents this by checking the ancestry of a reference before instantiation. If a cycle is detected—where Component A references Component B which eventually references Component A again—the system throws a VisualComponentRecursionError, halting both editor rendering and publishing to prevent infinite loops.
Instantiating Visual Components in the Editor
When a user drops a Visual Component onto the canvas, the editor invokes instantiateVCAtRef from src/core/visualComponents/instantiate.ts. This function:
- Resolves the component definition tree using the
componentIdstored on the reference node. - Executes
syncSlotInstancesto materialize the requiredbase.slot-instancechildren. - Flattens the component tree (including the new slot-instances) into a flat node map merged into the page's tree.
The instantiated content appears immediately in the editor via VCInlineTree (src/modules/base/visualComponentRef/VCInlineTree.tsx), allowing authors to edit slot content in place.
Publishing and Rendering Slot Fills
During static site generation, renderVisualComponentRef in src/core/publisher/renderVisualComponentRef.ts handles the final output:
- It looks up the referenced component definition.
- Builds a map of
base.slot-instancenodes under the reference, keyed byslotName. - Walks the component's definition tree recursively; when encountering a
base.slot-outlet, it retrieves the matching slot-instance's children and inlines them into the output. - The slot-outlet itself is transparent—it emits no HTML wrapper—so the final page contains only the consumer's content without structural artifacts.
UI Constraints for Locked Slot Instances
The editor enforces strict constraints on base.slot-instance nodes to prevent accidental corruption of the component contract. In src/admin/pages/site/panels/DomPanel/LayerNodeContextMenu.tsx, destructive actions like rename, delete, and reorder are disabled for these nodes. The SlotInstanceEditor (src/modules/base/slotInstance/SlotInstanceEditor.tsx) renders a header displaying the slot name and a container for editable children, while SlotOutletEditor renders only a thin placeholder in the component definition canvas.
Summary
- Visual Components are referenced via
base.visual-component-refnodes that point to reusable component definitions. - Slots are declared inside components using
base.slot-outletnodes with uniqueslotNameproperties. - Slot Instances are auto-generated, locked
base.slot-instancenodes that hold consumer content and synchronize automatically with the component's slot definitions. syncSlotInstancescomputes insert, rename, and delete operations to keep slot instances aligned with the component definition.applySlotSyncResultapplies these changes atomically to maintain tree consistency.- Recursion guards prevent cyclic component references from crashing the editor or build process.
- Publishing transparently injects slot content into outlet positions, emitting clean HTML without wrapper elements.
Frequently Asked Questions
How does Instatic prevent slot content from getting out of sync when a component is updated?
Instatic runs syncSlotInstances whenever a Visual Component reference is created or the component definition changes. This function compares the current set of base.slot-outlet names in the component against existing base.slot-instance children, automatically inserting new slots, renaming existing ones to match updated names, and deleting orphaned instances while preserving other content.
Can users manually delete or rename a slot instance in the page tree?
No. base.slot-instance nodes are created with a locked: true flag that disables destructive actions in the DOM panel. Users can only edit the children inside the slot instance; they cannot move, rename, or delete the container itself. This ensures the slot hierarchy remains consistent with the component definition.
What happens if a Visual Component references itself or creates a circular reference?
The recursion guard in src/core/visualComponents/recursionGuard.ts checks the ancestry chain before instantiating any Visual Component reference. If it detects a cycle—for example, Component A referencing Component B which references Component A—it throws a VisualComponentRecursionError, preventing both the editor and the publisher from entering an infinite loop.
How is slot content rendered in the final static HTML?
During publishing, renderVisualComponentRef builds a map of slot instances by name, then walks the component definition tree. When it encounters a base.slot-outlet, it looks up the matching slot instance and renders its children in place of the outlet. The outlet node itself produces no HTML output, ensuring the final page contains only the author's content without extra wrapper elements.
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 →