# How Page Tree Mutations Function with Instatic's NodeTree Primitive

> Learn how Instatic's NodeTree primitive handles page tree mutations with pure, Mutative-compatible helper functions for O(1) lookups and cycle-safe structural changes using Zustand.

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

---

**Page tree mutations in Instatic are handled by pure, Mutative-compatible helper functions that directly modify a flat `NodeTree<PageNode>` structure, enabling O(1) parent lookups and cycle-safe structural changes via Zustand's draft-based state management.**

Instatic represents every page and visual component as a `NodeTree<PageNode>`—a flat data structure defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts). All structural modifications, whether triggered by the visual editor, plugins, or server-side logic, flow through a centralized set of mutation helpers in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). These functions provide the only mechanism for altering the page tree, ensuring consistent behavior and maintaining structural integrity across the application.

## The NodeTree Primitive Architecture

The **NodeTree** primitive eschews traditional nested trees in favor of a flat map design. Each tree maintains a `nodes` field—typed as `Record<string, PageNode>`—that maps unique node IDs to their corresponding `PageNode` objects, alongside a `rootNodeId` string that anchors the hierarchy.

This flat architecture enables constant-time lookups. Instead of recursive traversal to find parents or siblings, operations leverage the `Record` structure for O(1) access. The `NodeTree` shape in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts) serves as the single source of truth for the entire page structure.

## Core Mutation Helpers

All structural changes are performed by pure helper functions defined in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts). Each function receives the draft `NodeTree` (typically from Zustand's Mutative middleware) and applies changes directly. These same functions can operate on `structuredClone` copies for functional-style pure calls when needed.

### Creating and Inserting Nodes

The foundation of tree manipulation begins with node creation and placement. The `createNode` function generates a fresh `PageNode` with a nano-generated ID, empty children array, and no parent assignment.

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

// Generate a new hero module node
const newNode = createNode('module:hero', { title: 'Welcome' });

// Append it to the root node
insertNode(pageTree, newNode, pageTree.rootNodeId);

```

The `insertNode` helper in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) handles parent linking automatically. When inserting at a specific index (or appending when omitted), it stamps the `parentId` on the child node and updates the parent's `children` array.

### Deleting and Moving Nodes

Structural reorganization relies on safe deletion and reparenting logic. The `deleteNode` function recursively removes a node and its entire subtree, unlinking it from its parent while cleaning up the nodes map.

For reparenting, `moveNode` guards against cycles by first checking ancestry relationships. It utilizes selector utilities from [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts)—specifically `getParent` for O(1) parent retrieval and `isAncestor` for depth-based cycle detection.

```typescript
import { moveNode, deleteNode } from '@core/page-tree';

// Move node to new parent at index 1
moveNode(pageTree, 'node-id', 'new-parent-id', 1);

// Remove entire subtree
deleteNode(pageTree, 'node-id');

```

### Updating Properties and Metadata

Node content and visibility are managed through targeted update functions. The `updateNodeProps` helper shallow-merges patch objects into a node's `props` field, while specialized functions handle metadata:

- **`renameNode`** updates the `label` field
- **`toggleNodeLocked`** flips the `locked` boolean
- **`toggleNodeHidden`** flips the `hidden` boolean
- **`setBreakpointOverride`** and **`clearBreakpointOverride`** manage per-breakpoint prop overrides stored in `breakpointOverrides`

```typescript
import { updateNodeProps, renameNode, setBreakpointOverride } from '@core/page-tree';

// Update props
updateNodeProps(pageTree, nodeId, { subtitle: 'Hello world' });

// Rename the node
renameNode(pageTree, nodeId, 'Hero Banner');

// Apply responsive override
setBreakpointOverride(pageTree, nodeId, 'mobile', { fontSize: 14 });

```

### Advanced Operations: Duplication, Wrapping, and Pasting

Complex editing workflows require batch operations that preserve tree integrity. The `duplicateNode` function deep-clones an entire subtree, generating new IDs for every cloned node before inserting the duplicate adjacent to the original.

For clipboard operations, `pasteSubtree` remaps IDs from foreign subtrees and optionally filters class IDs before insertion. The `wrapNode` and `wrapNodes` functions surround selections with new container modules, preserving child order and updating parent references.

```typescript
import { duplicateNode, wrapNode, pasteSubtree } from '@core/page-tree';

// Clone a node and its children
const cloneId = duplicateNode(pageTree, originalId);

// Wrap in a section container
const wrapperId = wrapNode(pageTree, nodeId, 'module:section', {});

// Paste clipboard content
pasteSubtree(pageTree, clipboardData, targetParentId);

```

## The Operation Dispatcher

A single entry point unifies all mutations for external consumers. The `applyTreeOperation` function in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) accepts a discriminated-union `TreeOperation` (defined in [`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts)) and routes it to the appropriate helper.

This dispatcher returns both the mutated tree and an array of `affectedNodeIds`, enabling precise cache invalidation and UI updates. It serves as the bridge between the visual editor, plugin VM boundary, and core mutation logic.

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

const operation = { 
  kind: 'renameNode', 
  nodeId: 'abc-123', 
  name: 'Updated Label' 
};

const result = applyTreeOperation(pageTree, operation);
// result.affectedNodeIds contains ['abc-123']

```

## Integration with Zustand and Mutative

The mutation helpers are designed for compatibility with Zustand's Mutative middleware. When invoked inside `mutateActiveTree` actions (located in [`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts)), they operate on drafts without returning new objects. The middleware records changes and produces immutable snapshots for the React component tree.

Alternatively, the same functions accept deep-cloned trees (via `structuredClone`) for pure functional operations outside the store. This dual compatibility supports both the editor's interactive state management and server-side tree transformations.

## Summary

- **Page tree mutations** in Instatic operate on a flat `NodeTree<PageNode>` structure defined in [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts), using `Record`-based storage for O(1) node access.
- All changes flow through pure helper functions in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), including `createNode`, `insertNode`, `moveNode`, and `duplicateNode`.
- Selector utilities in [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) provide efficient parent lookup and ancestry checking to prevent cycles.
- The `applyTreeOperation` dispatcher unifies mutations via a `TreeOperation` union type, returning affected node IDs for cache management.
- Mutative compatibility allows these functions to work seamlessly with Zustand drafts or `structuredClone` copies, supporting both interactive editing and functional programming patterns.

## Frequently Asked Questions

### How does Instatic prevent circular references when moving nodes?

The `moveNode` function prevents cycles by utilizing the `isAncestor` selector from [`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts) before reparenting. This check traverses upward from the target parent to verify the moved node is not already an ancestor, ensuring the tree remains a directed acyclic graph.

### What is the difference between `insertNode` and `pasteSubtree`?

While `insertNode` adds a single existing node to a parent at a specified index, `pasteSubtree` handles foreign subtrees—such as those from the clipboard—by remapping all node IDs to prevent collisions and optionally filtering class IDs. `pasteSubtree` is designed for cross-document operations, whereas `insertNode` manages intra-tree restructuring.

### Can these mutation functions be used outside of the Zustand store?

Yes. Although optimized for Zustand's Mutative middleware, all mutation helpers in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) are pure functions that can operate on any `NodeTree` instance. Developers can pass a `structuredClone` of the tree for functional-style mutations, making them suitable for server-side rendering or background workers.

### Where are breakpoint-specific property overrides stored in the node tree?

Breakpoint overrides are stored in the `breakpointOverrides` field of each `PageNode`. The `setBreakpointOverride` and `clearBreakpointOverride` functions in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) manage these values, allowing responsive prop values to persist within the flat tree structure without affecting the base `props` object.