Visual Components in Instatic: How Slots Manage Content
Visual Components in Instatic are server-side reusable UI building blocks defined by TypeBox schemas, while slots act as declarative outlet points synchronized via slot-instance nodes to bridge component definitions with consumer content.
Visual Components (VCs) in the Instatic framework provide a powerful abstraction for creating reusable, server-side UI elements. This article explores how these components are structured and how the slot system manages dynamic content insertion, based on the CoreBunch/Instatic source code.
What Are Visual Components in Instatic?
Visual Components are first-class objects that live on the server and serve as reusable UI building blocks. Each VC is defined by the VisualComponentSchema TypeBox schema located in src/core/visualComponents/schemas.ts, which describes three primary parts:
tree: A flat-map node tree (NodeTree<VCNode>) describing the component's internal structure. These nodes use the same shape as regular page nodes (BaseNode) but exclude anydynamicBindings.params: An array of VCParams (VCParam) declaring configurable inputs such as strings, numbers, colors, or slots. Each parameter guarantees a stableid, uniquename, and specificVCParamType.classIds/createdAt: Metadata fields used for styling and auditing purposes.
The schemas serve as the single source of truth, with TypeScript types derived via Static<typeof VisualComponentSchema>. Parsing is handled by parseVisualComponent, which tolerates malformed nodes by dropping them without breaking the entire component.
A VC is instantiated on a page through a visual-component-ref node (base.visual-component-ref). This reference node stores the componentId of the VC it embeds. During editing, the editor reads the VC's definition and constructs slot-instance children for each slot parameter, synchronizing these children with the VC's slot outlets.
How Slots Manage Content in Instatic
Slots provide the mechanism for Visual Components to expose insertion points for consumer content. The system maintains a strict one-to-one relationship between slot declarations and slot instances through a synchronization process.
Slot Declaration in Component Trees
Inside a VC's internal tree, slot outlets are declared using nodes with moduleId === 'base.slot-outlet'. The props.slotName property identifies the slot, defaulting to "children" when omitted. The collectSlotOutletNames function in src/core/visualComponents/slotSync.ts traverses the VC tree via DFS pre-order to gather unique slot names.
import { collectSlotOutletNames } from '@core/visualComponents/slotSync';
// vc is the parsed Visual Component
const slotNames = collectSlotOutletNames(vc.tree);
// Returns: ['icon', 'label'] or ['children'] if default
Slot-Instance Synchronization
When a VC reference appears on a page, the editor ensures the ref maintains exactly one base.slot-instance child per declared slot, ordered according to slot-outlet appearance in the definition. The pure function syncSlotInstances computes the minimal set of operations—insert, rename, or delete—required to synchronize the ref's children with the VC's current slot definitions.
import { syncSlotInstances } from '@core/visualComponents/slotSync';
// Returns operations needed to align children with slot definitions
const result = syncSlotInstances(vcRefNode, vc, tree.nodes);
Applying Slot Updates
The applySlotSyncResult function mutates the mutable node map within a Mutative recipe. It inserts new slot-instance nodes, renames or deletes mismatched ones, and updates the ref's children array to the ordered list of slot-instance IDs. This logic is implemented in src/core/visualComponents/slotSync.ts between lines 66-100.
import { applySlotSyncResult } from '@core/visualComponents/slotSync';
import { mutateActiveTree } from '@admin/pages/site/store/slices/site/helpers';
mutateActiveTree((tree) => {
const result = syncSlotInstances(vcRefNode, vc, tree.nodes);
applySlotSyncResult(tree.nodes, result, vcRefNode.id);
});
Consumer Content Rendering
The slot-instance node exists as a normal child in the page's node tree, allowing editors to place any nodes—text, images, or even other Visual Components—underneath it. At render time, the publisher matches each base.slot-instance with its corresponding base.slot-outlet in the VC definition, stitching the consumer subtree into the VC's layout.
Practical Implementation: Working with Visual Components and Slots
The following workflow demonstrates how to parse a stored Visual Component, identify its slots, and synchronize a page reference:
// 1. Parse a stored Visual Component from the database
import { parseVisualComponent } from '@core/visualComponents/schemas';
import { getComponentRow } from '@core/data/componentFromRow';
const row = await getComponentRow('my-button');
const vc = parseVisualComponent(row?.data);
if (!vc) throw new Error('Invalid component');
// 2. Identify declared slot names
const slotNames = collectSlotOutletNames(vc.tree);
// 3. Synchronize the VC reference on the page
mutateActiveTree((tree) => {
const result = syncSlotInstances(vcRefNode, vc, tree.nodes);
applySlotSyncResult(tree.nodes, result, vcRefNode.id);
});
Core Files and Architecture
The Visual Component and slot system spans several key files in the Instatic codebase:
src/core/visualComponents/schemas.ts: Defines the TypeBox schema for Visual Components, parameter types, and tolerant parsers likeparseVisualComponent.src/core/visualComponents/vcRefs.ts: Centralizes detection ofbase.visual-component-refnodes and theircomponentIdextraction.src/core/visualComponents/slotSync.ts: Implements slot-outlet discovery viacollectSlotOutletNames, synchronization logic viasyncSlotInstances, and mutation application viaapplySlotSyncResult.src/core/page-tree-schema.ts: Provides sharedBaseNodedefinitions and tree utilities used across VCs.src/__tests__/visualComponents/vcRefs.test.ts: Contains the test suite verifying correct VC reference handling.
Summary
- Visual Components in Instatic are server-side UI blocks defined by
VisualComponentSchema, comprising a node tree, parameters, and metadata. - Slots are declared via
base.slot-outletnodes within the VC tree, identified byprops.slotName(defaulting to"children"). - The
collectSlotOutletNamesfunction extracts slot identifiers from component definitions using DFS pre-order traversal. syncSlotInstancescalculates minimal operations to align page references with current slot definitions, whileapplySlotSyncResultexecutes these mutations.- Slot-instances act as consumer-side placeholders that hold actual content, maintaining a one-to-one relationship with slot-outlets through automated synchronization.
Frequently Asked Questions
What is the difference between a slot-outlet and a slot-instance?
A slot-outlet is a declaration inside the Visual Component's internal tree (base.slot-outlet) that marks where content should be inserted. A slot-instance is a concrete node (base.slot-instance) created on the page as a child of the VC reference, serving as the container for consumer-provided content. The outlet defines the insertion point; the instance holds the actual content.
How does Instatic handle malformed Visual Component data?
The parseVisualComponent function in src/core/visualComponents/schemas.ts implements tolerant parsing. When encountering malformed nodes or invalid data structures, it drops the problematic nodes without throwing errors or breaking the entire component. This ensures that partially corrupted components can still render their valid portions while isolating defects.
Can Visual Components contain other Visual Components?
Yes. Because slot-instances are standard nodes in the page tree, they can contain any valid node types, including other base.visual-component-ref nodes. This enables nested composition where a Visual Component can embed additional Visual Components within its slots, creating complex, hierarchical layouts while maintaining the slot synchronization guarantees at each level.
What happens when a Visual Component's slot structure changes?
When a VC definition is modified—such as adding, removing, or reordering slots—the syncSlotInstances function detects these changes during the next edit session. It computes the minimal set of operations to reconcile the existing page structure with the new definition: inserting missing slot-instances, removing obsolete ones, or renaming slots to match new identifiers. The applySlotSyncResult function then applies these changes to maintain tree consistency.
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 →