# How Instatic Auto-Detects Dynamic Nodes During Publishing: Core Rules and Implementation

> Discover how Instatic auto-detects dynamic nodes with five core rules. Learn how the publisher efficiently determines static content versus request-time placeholders for optimized publishing.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-03

---

**Instatic classifies every node in a page tree as static or dynamic using a single walker in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), which applies five detection rules to determine whether content can be baked at publish time or must be deferred to request-time `<instatic-hole>` placeholders.**

The Instatic publisher relies on **auto-detection of dynamic nodes** to decide which parts of a page tree can be rendered as static HTML files versus which require runtime hydration. This classification system, implemented entirely within [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), analyzes module definitions, binding sources, and component references to build a definitive set of node IDs that must be treated as dynamic.

## The Dynamic Detection Walker

The core architecture centers on a classification walker that traverses the page tree and applies a consistent rule set to every node. According to the Instatic source code, this walker ensures that **Layer A** (static baking) and **Layer C** (placeholder emission) use identical logic, preventing drift between build-time and runtime behavior.

The detection process begins with the `findDynamicNodesWithReasons` function, which executes a pre-pass for loop promotion followed by the main classification pass. A simpler wrapper, `findDynamicNodeIds`, returns only the set of dynamic node IDs for production use.

## The Five Detection Rules

The walker applies five specific rules to classify nodes. Each rule targets a different mechanism by which content can become request-dependent.

### Rule 1: Explicit Dynamic Flag

If a module’s definition in the registry contains `dynamic: true`, the node is immediately marked as dynamic. In [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), the `classifyNode` function checks `def?.dynamic` at lines 87–91 to enforce this rule.

```typescript
// Conceptual implementation from the source
if (moduleDef?.dynamic === true) {
  return { dynamic: true, reason: 'Module explicitly marked as dynamic' };
}

```

### Rule 2: Dynamic Bindings and Inline Tokens

Nodes may declare a `dynamicBindings` map or contain inline binding tokens in string properties. The system examines binding sources and fields to determine if they depend on request-time data.

**Structured bindings**: The `checkDynamicBindings` function iterates over `node.dynamicBindings` and uses `isBindingSourceRequestDependent` (lines 107–115) to test whether the source is request-dependent, such as `route.query.*`.

**Inline tokens**: For string properties containing `{source.field}` tokens, `checkInlineTokens` parses the strings, detects tokens, and calls `isBindingSourceRequestDependent` (lines 121–129). If any token’s source is request-dependent, the node becomes dynamic.

### Rule 3: Request-Dependent Loop Sources

For nodes of module `base.loop`, the walker examines the `sourceId` property. If the referenced loop source in `loopSourceRegistry` is marked with `requestDependent: true` or `perVisitor: true`, the node is classified as dynamic. The `checkLoopSource` function implements this check at lines 140–150.

### Rule 4: Visual Component References

A `base.visual-component-ref` node is dynamic if *any* node inside the referenced visual component (VC) definition tree is dynamic. The algorithm recursively walks the VC’s tree using `collectSubtreeReasons`, guarding against cycles, and propagates the first inner reason found. This logic appears in `classifyNode` at lines 200–218.

```typescript
// VC reference handling in classifyNode
if (node.moduleId === 'base.visual-component-ref') {
  const vcTree = loadVisualComponent(node.refId);
  const innerReason = collectSubtreeReasons(vcTree, visited);
  if (innerReason) {
    return { dynamic: true, reason: innerReason };
  }
}

```

### Rule 3.5: Loop Body Promotion (Pre-Pass)

Before the main classification pass, the walker executes a promotion phase for loop bodies. Static loops whose bodies contain request-dependent nodes are promoted to a single dynamic hole. Their inner node IDs are suppressed so the publisher does not emit separate holes for each iteration. This pre-pass logic in `findDynamicNodesWithReasons` builds `promotedLoops` and `suppressed` sets at lines 113–124.

## Public API and Publisher Integration

The detection system exposes two public helpers consumed by the publisher orchestrator in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts):

**`findDynamicNodeIds(page, site, registry)`** – Returns a `Set` of page-level node IDs that must become holes. This lightweight function is used for the static-versus-dynamic shortcut decision.

**`findDynamicNodesWithReasons(page, site, registry)`** – Returns both the ID set and a list of human-readable diagnostic reasons.

The publisher orchestrator invokes these functions during page rendering:

```typescript
import { findDynamicNodeIds } from './dynamicDetection';

const dynamicIds = findDynamicNodeIds(page, site, registry);
if (dynamicIds.size === 0) {
  // Page is fully static → bake complete HTML file (Layer A)
} else {
  // Render with placeholders (Layer C)
}

```

For Layer C’s `renderNode`, the same `dynamicIds` set drives placeholder emission:

```typescript
if (dynamicIds.has(node.id)) {
  // Emit <instatic-hole> instead of fully rendering
}

```

## Layer Architecture Impact

The auto-detection results feed into three distinct publishing layers:

- **Layer A**: Bake fully-static pages to `uploads/published/current/`. Checks if `dynamicIds` is empty to decide between *shell* (static) and *complete* (requires holes) rendering.
- **Layer B**: In-memory LRU cache for dynamic routes. Stores cached results containing hole placeholders generated by Layer C.
- **Layer C**: Emit `<instatic-hole>` placeholders. Walks the `dynamicIds` set to insert runtime-fetch holes for each dynamic node.

## Practical Implementation Examples

### Debugging Dynamic Classification

Use `findDynamicNodesWithReasons` to inspect why specific nodes are classified as dynamic:

```typescript
import { findDynamicNodesWithReasons } from '@core/publisher/dynamicDetection';

function logDynamicReasons(page, site, registry) {
  const { dynamicPageNodeIds, reasons } = findDynamicNodesWithReasons(page, site, registry);
  console.log('Dynamic node IDs:', [...dynamicPageNodeIds]);
  console.log('Why they are dynamic:');
  reasons.forEach(r => console.log(' –', r));
}

```

### Extending Request-Dependent Sources

When introducing new binding sources that should be treated as request-dependent, extend the `isBindingSourceRequestDependent` function:

```typescript
import { isBindingSourceRequestDependent } from '@core/publisher/dynamicDetection';

function registerMySource() {
  const original = isBindingSourceRequestDependent;
  isBindingSourceRequestDependent = (source, field) => {
    if (source === 'myCustomSource') return true;
    return original(source, field);
  };
}

```

### Server Handler Integration

Implement publish-time logic that branches based on dynamic detection:

```typescript
import { findDynamicNodeIds } from '@core/publisher/dynamicDetection';
import { renderPage } from '@core/publisher/render';

export async function handlePublish(req, res) {
  const { page, site, registry } = await loadPageContext(req);
  const dynamicIds = findDynamicNodeIds(page, site, registry);

  if (dynamicIds.size === 0) {
    const html = renderPage(page, site, registry);
    await writeStaticFile(page.path, html);
  } else {
    const html = renderPage(page, site, registry); // Automatically inserts holes
    await writeStaticFile(page.path, html);
  }
}

```

## Summary

- **Single source of truth**: All dynamic node detection logic resides in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), ensuring Layer A and Layer C use identical classification rules.
- **Five rule categories**: Detection covers explicit dynamic flags, structured bindings, inline tokens, loop sources, and recursive visual component references.
- **Pre-pass optimization**: Loop body promotion prevents excessive hole emission by consolidating dynamic inner nodes into a single parent hole.
- **Dual API surface**: `findDynamicNodeIds` provides production-ready ID sets, while `findDynamicNodesWithReasons` offers diagnostic capabilities for debugging.
- **Request-time dependency**: The core distinction between static and dynamic hinges on whether a node depends on request-time data such as `route.query.*` or per-visitor loops.

## Frequently Asked Questions

### How does Instatic handle nested visual components with mixed static and dynamic children?

When a `base.visual-component-ref` node is encountered, the walker recursively analyzes the entire referenced component tree using `collectSubtreeReasons` (lines 200–218 in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts)). If any node within that tree is dynamic, the VC reference itself is marked as dynamic. This prevents partial static baking of components that contain dynamic elements, ensuring runtime consistency.

### What happens if a static loop contains a dynamic node in its body?

The system runs a pre-pass (Rule 3.5) before main classification that detects static loops with request-dependent inner nodes. These loops are "promoted" to dynamic status, and their inner node IDs are added to a suppression set. This ensures the publisher emits a single `<instatic-hole>` for the loop rather than separate holes for each iteration, optimizing the output structure.

### Can I override the auto-detection for specific nodes?

While the core walker does not provide a bypass mechanism, you can influence classification by adjusting module definitions in the registry. Setting `dynamic: true` in a module definition (Rule 1) forces dynamic classification, while ensuring no request-dependent bindings or sources are present keeps a node static. For custom binding sources, extend `isBindingSourceRequestDependent` to include your source as request-dependent.

### Where does the publisher actually insert the `<instatic-hole>` placeholders?

Layer C of the publisher, implemented in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts), consumes the `dynamicIds` set returned by `findDynamicNodeIds`. During the rendering traversal, when `renderNode` encounters a node ID present in this set, it emits an `<instatic-hole>` placeholder instead of the fully rendered content, deferring that node's rendering to request time.