# How Instatic Auto-Detects Dynamic Nodes and Generates Placeholders During Publishing

> Instatic publisher automatically detects dynamic nodes using four rules and replaces them with placeholders. Learn how Instatic enables static HTML generation with client-side hydration.

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

---

**The Instatic publisher identifies request-dependent nodes at build time using four detection rules and replaces them with `<instatic-hole>` placeholders, enabling static HTML generation with client-side lazy hydration.**

When publishing pages in the [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic) repository, the system must distinguish between static content that can be baked into HTML and dynamic content that varies per request. The `findDynamicNodeIds` function in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) performs this analysis, returning a set of node IDs that trigger placeholder insertion during rendering.

## How Dynamic Node Detection Works

The detection pipeline walks the `NodeTree` representing the page's component hierarchy and applies four independent rules. Any node matching at least one rule is marked dynamic.

### The Four Detection Rules

Located in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), the algorithm checks each node for:

- **`base.live` flag** — Nodes flagged for live-editing by the collaborative Yjs engine are dynamic because their content changes on every request.

- **`base.slot-instance` with children** — Slot fills stored as separate nodes that may be edited independently require dynamic treatment when non-empty.

- **Data-source references** — Components with `base.fetch` definitions are marked dynamic since fetched payloads differ per request.

- **Explicit `base.dynamic: true`** — Authors can force dynamic treatment for any component via this marker property.

The function returns a `Set<string>` of node IDs requiring placeholder substitution.

## Placeholder Generation and Rendering

After detection completes, the render step in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts) substitutes each dynamic node with a lightweight custom element:

```tsx
// src/core/publisher/render.ts (excerpt)
if (dynamicNodeIds.has(node.id)) {
  return <instatic-hole nodeId={node.id} />
}

```

The `<instatic-hole>` element serves as a hydration marker. The client-side hole loader—implemented in [`src/core/publisher/holeLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/holeLoader.ts)—registers an `IntersectionObserver` that fetches actual fragments from `/_instatic/hole/<nodeId>` when placeholders enter the viewport.

## Code Examples

### Finding Dynamic Node IDs

```typescript
// Simplified usage of the detection API
import { NodeTree } from '@core/page-tree';
import { findDynamicNodeIds } from '@core/publisher/dynamicDetection';

export function getDynamicIds(tree: NodeTree): Set<string> {
  // Returns node IDs needing placeholders
  return findDynamicNodeIds(tree);
}

```

### Rendering with Placeholders

```tsx
import { NodeTree } from '@core/page-tree';
import { findDynamicNodeIds } from '@core/publisher/dynamicDetection';

export function renderPage(tree: NodeTree) {
  const dynamicIds = findDynamicNodeIds(tree);

  function renderNode(node: any): JSX.Element {
    if (dynamicIds.has(node.id)) {
      // Insert placeholder for client-side lazy loading
      return <instatic-hole nodeId={node.id} />;
    }
    // Static rendering path
    return <div>{node.props.children}</div>;
  }

  return <>{tree.rootChildren.map(renderNode)}</>;
}

```

## Integration with the Publish Pipeline

The detection-and-placeholder step executes immediately before Layer-A static artifacts are written to `uploads/published/current/<route>.html`. The resulting HTML contains fully-rendered static markup mixed with `<instatic-hole>` tags for every request-dependent node.

This architecture delivers three key benefits:

- **Cacheability** — Static portions can be cached at the edge without invalidation from dynamic data changes.
- **Performance** — Initial HTML payloads remain small; dynamic fragments load on demand.
- **Flexibility** — Live-editing, personalization, and runtime data fetching operate without sacrificing static generation.

## Key Source Files

| File | Purpose |
|------|---------|
| [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) | Implements `findDynamicNodeIds` and the four detection rules |
| [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts) | Walks tree, substitutes dynamic nodes with `<instatic-hole>` |
| [`src/core/publisher/holeLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/holeLoader.ts) | Client-side IntersectionObserver for lazy fragment fetching |
| [`src/core/page-tree/treeSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/treeSchema.ts) | Defines `NodeTree` shape used throughout detection |
| [`src/core/publisher/dynamicDetection.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.test.ts) | Unit tests verifying detection logic |

## Summary

- **`findDynamicNodeIds`** in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts) identifies dynamic nodes using four rules covering live-editing, slots, data fetching, and explicit markers.
- **`<instatic-hole>` placeholders** replace dynamic nodes during rendering, enabling static HTML with deferred hydration.
- **Client-side hole loader** fetches fragments lazily via `IntersectionObserver`, balancing cacheability with dynamic functionality.
- The pipeline generates deployable static artifacts to `uploads/published/current/` while preserving runtime flexibility.

## Frequently Asked Questions

### What triggers a node to be marked as dynamic in Instatic?

A node becomes dynamic if it has the `base.live` flag, contains non-empty `base.slot-instance` children, references a `base.fetch` data source, or explicitly sets `base.dynamic: true`. The `findDynamicNodeIds` function checks all four conditions during the publish walk.

### How does the `<instatic-hole>` placeholder get replaced with real content?

The client-side hole loader in [`holeLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/holeLoader.ts) registers an `IntersectionObserver` on all `<instatic-hole>` elements. When a placeholder enters the viewport, the loader fetches the corresponding fragment from `/_instatic/hole/<nodeId>` and swaps it into the DOM.

### Where does the dynamic node detection run in the publish pipeline?

Detection executes immediately before static artifact generation, after the page tree is fully resolved but before HTML is written to `uploads/published/current/<route>.html`. This timing ensures placeholders are baked into the deployable output.

### Can authors force static treatment of a dynamic-looking node?

No direct bypass exists in the detection rules, but authors can remove the triggering conditions—clearing `base.live`, emptying slot fills, removing `base.fetch` references, or unsetting `base.dynamic`. The detection logic has no override mechanism; it strictly evaluates the four rules as implemented in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts).