# How Dynamic Detection Works in Instatic’s Publishing Pipeline

> Understand dynamic detection in Instatic's publishing pipeline. Learn how Instatic renders pages statically or uses placeholders for request-time hydration to optimize performance.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-07-31

---

**Dynamic detection in Instatic evaluates every page node against four hierarchical rules to determine whether content can be rendered statically at build time or must be replaced with an `<instatic-hole>` placeholder for request-time hydration.**

Instatic’s publishing pipeline must decide for every node in the page tree whether it can be safely rendered into static HTML or requires runtime resolution. This **dynamic detection** mechanism, implemented in the CoreBunch/Instatic repository, serves as the single source of truth that separates static generation from dynamic rendering, ensuring request-dependent data never leaks into cached markup.

## The Core Detection Engine

### Single Source of Truth in dynamicDetection.ts

All classification logic resides in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts). The file exports two public helpers that drive the entire pipeline:

- `findDynamicNodeIds(page, site, registry)` – returns a `Set<string>` of page-level node IDs that require holes.
- `findDynamicNodesWithReasons(page, site, registry)` – returns the same set plus diagnostic strings explaining why each node is dynamic.

Both functions walk the tree and apply detection rules in a single pass, providing the data necessary for the publisher to choose between static emission and placeholder injection.

### The Four Hierarchical Detection Rules

The classifier evaluates nodes sequentially using these rules, implemented at lines 14–24 of the detection file:

- **Rule 1: Explicit Dynamic Flag.** If the module definition in the registry has `dynamic: true`, the node is immediately marked as dynamic regardless of other properties.
- **Rule 2: Request-Dependent Bindings.** If a node contains a `dynamicBindings` entry where the source-field pair is request-dependent (e.g., `route.query.*`), the node requires runtime resolution. The helper `isBindingSourceRequestDependent` (lines 64–80) centralizes this definition; currently only `route.query` is treated as request-dependent, while `page`, `site`, and `currentEntry` are considered static.
- **Rule 2b: Inline Token Strings.** If a string prop contains an inline token `{source.field}` (parsed by `containsTokens` and `parseTokenString`) and the token’s source is request-dependent, the node is classified as dynamic.
- **Rule 3: Dynamic Loop Sources.** If the node is `base.loop` and its entry in `loopSourceRegistry` (defined in [`src/core/loops/types.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/loops/types.ts)) is marked `requestDependent: true` or `perVisitor: true`, the loop data changes per request or visitor, forcing dynamic rendering.
- **Rule 4: Recursive Visual Component References.** If the node is `base.visual-component-ref`, the detector recursively inspects the referenced Visual Component definition tree (provided by [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts), cycle-guarded). If any node within that VC tree is dynamic, the outer reference is marked dynamic to ensure proper boundary encapsulation.

## Pre-Pass Optimization for Static Loops (ISS-021)

Before the main classification pass, the detector runs a **pre-pass** (lines 315–222) to optimize partially dynamic loops. If a **static** `base.loop` contains at least one dynamic descendant, the loop node itself is promoted to a hole, and all its descendant IDs are added to a `suppressed` set. This prevents emitting separate `<instatic-hole>` elements for every inner dynamic node, dramatically reducing HTML fragmentation and runtime hydration overhead for lists with mixed static and dynamic content.

## Integration into the Publishing Pipeline

Dynamic detection drives two architectural layers of the publishing process:

- **Layer A – Shell vs. Complete Rendering:** Determines whether the entire page can be emitted as static HTML or requires a shell framework.
- **Layer C – `<instatic-hole>` Emission:** Injects placeholders for nodes classified as dynamic.

In [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts), the `publishPage` function invokes detection at the pipeline start (line 98):

```typescript
const dynamicNodeIds = findDynamicNodeIds(page, site, registry)

```

The resulting `Set` is stored in `RenderConfig` (line 113) and threaded through the tree walk to `renderNode`. When `renderNode` encounters a node whose ID exists in `dynamicNodeIds`, it emits an `<instatic-hole>` placeholder instead of recursing into that subtree. The same set informs the static-vs-complete decision at Layer A.

## Diagnostic Debugging with Reasons

For developers troubleshooting unexpected dynamic behavior, `findDynamicNodesWithReasons` provides human-readable explanations such as *“node 'btn1': binding 'label' source 'route.query.search' is request-dependent”*. These strings surface in the admin UI to pinpoint exactly which rule triggered the classification, enabling precise optimization of page staticity.

## Practical Code Examples

### Manual Detection in Plugins

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

function logDynamicInfo(page, site, registry) {
  const ids = findDynamicNodeIds(page, site, registry)
  console.log('Dynamic node IDs:', [...ids])

  const { reasons } = findDynamicNodesWithReasons(page, site, registry)
  console.log('Why they are dynamic:')
  reasons.forEach(r => console.log('- ', r))
}

```

### Custom Render Pass Optimization

```typescript
function customRender(page, site, registry) {
  const dynamicIds = findDynamicNodeIds(page, site, registry)

  // Skip heavy computation for dynamic nodes during preview
  for (const node of Object.values(page.nodes)) {
    if (dynamicIds.has(node.id)) continue   // becomes a hole at runtime
    // … render node normally …
  }
}

```

### Extending Request-Dependent Sources

To register a new request-dependent source like `user.session`, extend `isBindingSourceRequestDependent`:

```typescript
function isBindingSourceRequestDependent(source: string, field: string): boolean {
  if (source === 'user' && field === 'session') return true
  // keep existing switch …
}

```

Now bindings like `user.session.role` automatically trigger Rule 2 without modifying the core detection walker.

## Summary

- Dynamic detection runs from [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), providing the `findDynamicNodeIds` and `findDynamicNodesWithReasons` APIs that serve as the pipeline’s single source of truth.
- Four hierarchical rules classify nodes: explicit module flags, request-dependent bindings (including inline tokens), dynamic loop sources, and recursive Visual Component checks.
- A pre-pass optimization promotes static loops containing dynamic descendants into single holes to minimize placeholder count and HTML size.
- The resulting ID set drives both the shell vs. complete rendering decision (Layer A) and the `<instatic-hole>` emission (Layer C) integrated in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts).

## Frequently Asked Questions

### What makes a binding source request-dependent in Instatic?

Currently, only `route.query` is treated as request-dependent within `isBindingSourceRequestDependent` (lines 64–80). Any `dynamicBindings` entry or inline token referencing `route.query.*` will force the node into dynamic rendering. Plugin authors can extend this function to recognize additional request-time sources such as headers or session data.

### Why does Instatic promote static loops to holes instead of their dynamic children?

The pre-pass optimization (ISS-021) promotes a static `base.loop` to a single hole when it contains dynamic descendants to prevent emitting multiple `<instatic-hole>` elements for every item in a partially dynamic list. This reduces HTML payload size and improves runtime performance by consolidating hydration boundaries at the loop level rather than fragmenting the interior.

### How does Instatic handle dynamic content inside Visual Components?

For `base.visual-component-ref` nodes, the detector recursively traverses the referenced Visual Component definition tree (guarded against cycles using the registry). If any node within that VC tree is dynamic according to the four rules, the entire reference is marked dynamic. This ensures the outer page boundary correctly encapsulates the runtime-dependent subtree, preventing static leakage.

### Where can I find the test coverage for dynamic detection?

The complete test suite covering all four detection rules and the pre-pass promotion logic resides in [`src/__tests__/server/dynamicDetection.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/dynamicDetection.test.ts). These tests verify that the classifier correctly identifies dynamic nodes while respecting the suppression set for optimized loop rendering, ensuring the pipeline maintains static safety guarantees.