# How Page Trees and Component Trees Are Managed in Instatic

> Discover how Instatic streamlines page and component tree management using a unified NodeTree structure for consistent document model mutations. Learn more today.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-02

---

**Instatic unifies pages, visual components, and slot fills into a single homogeneous NodeTree structure that uses a flat map of nodes together with a rootNodeId, enabling consistent mutations across the entire document model.**

Instatic handles complex document hierarchies through a unified tree architecture rather than maintaining separate data structures for different entity types. Both **pages** and **visual components (VCs)** are implemented as **NodeTree** instances in the CoreBunch/Instatic repository, with the only distinctions being module IDs and metadata fields. This design powers the visual editor, component system, and static publishing pipeline through a single mutation engine and persistence schema.

## The Unified NodeTree Architecture

### NodeTree as the Core Primitive

At the foundation of Instatic’s document model lies the **NodeTree** primitive defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts). The tree is stored as a flat map of nodes (`Record<id, BaseNode>`) alongside a `rootNodeId` that anchors the hierarchy. This structure avoids deeply nested JSON and enables O(1) node lookups while maintaining parent-child relationships through explicit ID references.

### Extending Base Nodes for Pages and Components

Every node in the system extends the base shape declared in [`src/core/page-tree/baseNode.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/baseNode.ts), which includes common fields such as `id`, `moduleId`, `children`, and `props`. Specific node types add specialized metadata:

- **Pages** are NodeTrees that add `title`, `slug`, and `dynamicBindings` fields
- **Visual components** store their own NodeTree as `vc.tree`
- **Slot fills** persist as children of locked `base.slot-instance` nodes living directly inside the consumer page’s tree

This unified shape means operations like duplication, movement, and deletion work identically whether you are editing a page root or a nested component instance.

## Mutating Tree Structures

### Node-Level Mutation Helpers

The engine provides 11 pure “node-level” mutation helpers in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) that operate on a mutable draft supplied by Zustand’s Mutative middleware. These include:

- **`insertNode`** – Validates the parent and appends the node, updating the `parentId` reference
- **`deleteNode`** – Removes a node and its subtree
- **`moveNode`** – Relocates a node within the hierarchy
- **`duplicateNode`** – Clones a subtree using `cloneNodeWithRemap` from [`src/core/page-tree/cloneNode.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/cloneNode.ts)
- **`wrapNode`** – Creates a container around existing nodes (Figma-style grouping)

### Page-Level Operations

Actions affecting the page roster reside in [`src/core/page-tree/pageMutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/pageMutations.ts). Functions like `addPage`, `deletePage`, and `duplicatePage` reuse the node-level helpers. For example, `duplicatePage` clones an entire page by calling `cloneNodeWithRemap` on the page’s root node, ensuring all internal IDs are remapped while preserving the tree structure.

### The Operation Dispatcher

The editor and plugins communicate with the tree through a tagged-union `TreeOperation` dispatched to `applyTreeOperation` in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). This function branches to the appropriate helper and returns the mutated tree plus the `affectedNodeIds` that may need cache invalidation, providing a clean boundary between UI actions and state mutations.

## Visual Components and Slot Management

### Slot Instance Synchronization

When a visual component reference is dropped onto a page, `syncSlotInstances` in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) automatically materializes one `base.slot-instance` child for each slot param defined by the VC. These slot-instance nodes are **locked**—meaning they cannot be deleted or moved directly—and carry the user-authored content as ordinary children. The function also deletes stray slot instances when the VC’s slot definitions change, keeping the tree in sync with the component schema.

### Rendering Slot Fills

During publishing, the renderer walks the page tree, locates `base.slot-instance` children, and injects their grandchildren at the matching `base.slot-outlet` position. This logic lives in [`src/core/publisher/renderVisualComponentRef.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderVisualComponentRef.ts). By keeping slot fills as part of the same tree rather than a separate data structure, Instatic ensures that tree-wide operations like duplication and serialization remain consistent across page boundaries.

## Working with the Tree API

### Creating a New Page

Pages are created through the page-level API, which initializes a `base.body` root node:

```typescript
import { addPage } from '@core/page-tree/pageMutations'

const newPage = addPage(siteDocument, 'About us', 'about')

```

The `addPage` function creates a fresh `base.body` node via `createNode` and appends the page to `site.pages`.

### Inserting Nodes

To insert a component into a page tree:

```typescript
import { insertNode, createNode } from '@core/page-tree/mutations'

const buttonNode = createNode('base.button', { label: 'Click me' })
insertNode(tree, buttonNode, parentNodeId)

```

`insertNode` validates the parent and updates the `parentId` on the new node.

### Duplicating Subtrees

The editor’s duplicate command uses the tree mutation helper:

```typescript
import { duplicateNode } from '@core/page-tree/mutations'

const newRootId = duplicateNode(tree, originalNodeId)

```

This builds an ID map, clones every node with `cloneNodeWithRemap`, re-links parent IDs, and splices the clone into the parent’s children array immediately after the original.

### Wrapping Nodes

Group selections into containers using:

```typescript
import { wrapNodes } from '@core/page-tree/mutations'

const wrapperId = wrapNodes(tree, ['nodeA', 'nodeB'], 'base.container')

```

`wrapNodes` reduces the selection to top-level nodes, finds the closest common ancestor using helpers from [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts), creates a wrapper node, and moves the branches under it.

### Synchronizing Slots

Keep slot instances aligned with VC definitions:

```typescript
import { syncSlotInstances } from '@core/visualComponents/slotSync'

syncSlotInstances({
  vcNodeId: visualComponentRefId,
  vcTree: vcTree,
  targetTree: pageTree,
})

```

This ensures a `base.slot-instance` child exists for every slot param, removes extraneous children, and marks all slot-instance nodes as locked.

### Dispatching Operations from Plugins

Plugins send structured operations through the central dispatcher:

```typescript
import { applyTreeOperation } from '@core/page-tree/mutations'

const result = applyTreeOperation(pageTree, {
  kind: 'moveNode',
  nodeId: 'node123',
  parentId: 'parent456',
  index: 2,
})

```

The result contains `affectedNodeIds` indicating which nodes require cache invalidation.

## Summary

- **Unified model**: Pages and visual components both use the same `NodeTree` primitive defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts), differing only in module IDs and metadata.
- **Flat architecture**: Trees are stored as flat maps (`Record<id, BaseNode>`) with a `rootNodeId`, enabling O(1) lookups and efficient mutations.
- **Pure mutations**: Eleven node-level helpers in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) handle insertion, deletion, movement, wrapping, and duplication through Zustand’s Mutative middleware.
- **Page operations**: [`src/core/page-tree/pageMutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/pageMutations.ts) provides CRUD for pages while reusing node-level logic like `cloneNodeWithRemap`.
- **Slot management**: `syncSlotInstances` in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) maintains locked `base.slot-instance` nodes that bridge VC definitions with user content, rendered by the publisher in [`src/core/publisher/renderVisualComponentRef.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderVisualComponentRef.ts).

## Frequently Asked Questions

### How does Instatic handle deeply nested component hierarchies?

Instatic treats nested components as ordinary node subtrees. When a visual component contains another VC reference, it stores its own `NodeTree` at `vc.tree`, and the parent page stores a `base.visual-component-ref` node. Slot fills are nested as children of `base.slot-instance` nodes. Because all operations use the same mutation engine in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), the depth of nesting does not require special handling—the tree utilities `getParent`, `isAncestor`, and `findClosestCommonAncestor` from [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) traverse the flat node map efficiently regardless of depth.

### What prevents users from accidentally deleting slot containers?

Slot-instance nodes are marked as **locked** during synchronization by `syncSlotInstances` in [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts). The editor UI respects this lock state, preventing direct deletion or movement of these container nodes. Users can only edit the user-authored content stored as ordinary children of the slot instance, ensuring the structural integrity required by the visual component’s definition remains intact.

### How are tree mutations optimized for performance in large documents?

Mutations operate on a mutable draft produced by Zustand’s Mutative middleware, allowing direct modifications without immutable copying overhead. The flat `Record<id, BaseNode>` structure means `getParent` and ancestry checks in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) run in constant time by ID lookup rather than tree traversal. Additionally, `applyTreeOperation` returns only the `affectedNodeIds`, enabling precise cache invalidation rather than triggering re-renders of the entire document tree.

### Can nodes be moved between different pages?

The current mutation API in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) operates within a single `NodeTree` instance. Moving content between pages requires extracting the subtree via `duplicateNode` (which uses `cloneNodeWithRemap` to generate new IDs) and then inserting it into the target page’s tree, followed by deletion from the source. Cross-page operations are handled at the application layer in [`src/core/page-tree/pageMutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/pageMutations.ts) to ensure ID remapping and reference consistency across the site document.