# How Instatic's Publisher Auto-Detects Dynamic Content for Hole Lazy-Loading

> Discover how Instatic's publisher auto-detects dynamic content for hole lazy-loading by applying four deterministic rules to classify nodes. Learn more about this core feature.

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

---

**Instatic's publisher automatically identifies request-dependent content by traversing the page-tree in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) and applying four deterministic classification rules to determine which nodes must be rendered as `<instatic-hole>` placeholders for lazy-loading.**

The Instatic static site generator (CoreBunch/Instatic) uses a two-phase publishing architecture where Phase A generates static shells at build time and Phase C inserts deferred placeholders for content that cannot be resolved until request time. The mechanism that powers this **auto-detection for hole lazy-loading** lives in a single source of truth: the `findDynamicNodeIds` function. This system walks the page-tree node by node, applying deterministic rules to decide exactly which components require lazy-loaded rendering.

## The Four Auto-Detection Rules in dynamicDetection.ts

The detection logic resides in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), where the `classifyNode` function implements four ordered rules (numbered 1-4 with an additional Rule 2b) to determine if a node must be deferred to request time. These rules are documented in the comment block at lines 14-26 of the file.

### Rule 1: Explicitly Dynamic Modules

If the **module definition in the registry** has `dynamic: true` set, the node is immediately flagged as dynamic. This allows module authors to declare that a component is inherently request-dependent, bypassing further analysis.

### Rule 2: Request-Dependent Bindings

A node becomes dynamic if it contains a **`dynamicBindings`** entry whose **source** is request-dependent. The helper function `isBindingSourceRequestDependent` encodes the built-in request-dependent sources (such as `route.query.*`) and can be extended via plugins.

**Rule 2b** extends this to string props containing interpolation tokens like `{source.field}`. If the token's source is request-dependent, the entire prop requires runtime interpolation, making the node dynamic.

### Rule 3: Request-Dependent Loop Sources

Nodes of type **`base.loop`** are marked dynamic when their registered loop source is marked `requestDependent: true` or `perVisitor: true` in the loop registry. This ensures that iteration logic depending on the specific visitor or request context is deferred to Phase C.

### Rule 4: Recursive Visual Component Analysis

If a node is a **`base.visual-component-ref`**, the detector recursively examines the visual component's definition tree. If any descendant within that tree is dynamic (with cycle detection to prevent infinite loops), the entire visual component reference is promoted to a hole. This ensures encapsulation of dynamic subtrees.

## Page-Tree Traversal Logic

The `findDynamicNodesWithReasons` function orchestrates two distinct passes over the page-tree to ensure consistent classification while optimizing the number of holes generated.

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

The first pass inspects **static `base.loop` bodies** (lines 107-122 in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts)). If any descendant of a loop body is dynamic, the entire loop node is promoted to a hole, and the inner dynamic IDs are **suppressed**. This prevents emitting numerous individual holes for items within a dynamic list (addressing issue ISS-021) and keeps the shell HTML cleaner.

### Main Pass: Node Classification

The second pass (lines 124-141) walks every page-node through the shared `classifyNode` function. When a node is classified as dynamic, its ID is added to the `dynamicPageNodeIds` set and a human-readable reason string is captured for diagnostics. Both passes use the same rule implementation, guaranteeing that the shell-vs-complete decision made during Phase A aligns perfectly with the placeholder emission in Phase C.

## Public API for Dynamic Node Detection

The module exports two primary functions from [`src/core/publisher/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/index.ts) for consumption by the rendering pipeline:

- **`findDynamicNodeIds(page, site, registry)`**: Returns a `Set` of page-node IDs that must be rendered as `<instatic-hole>` placeholders. This is the high-performance API used during publishing.

- **`findDynamicNodesWithReasons(page, site, registry)`**: Returns an object containing both the `dynamicPageNodeIds` set and a `reasons` array of diagnostic strings explaining why each node was flagged. Use this for debugging and plugin development.

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

// Standard usage during publish pass
const dynamicIds = findDynamicNodeIds(page, site, moduleRegistry)

// Diagnostic usage for debugging
const { dynamicPageNodeIds, reasons } = findDynamicNodesWithReasons(
  page,
  site,
  moduleRegistry
)

console.log('Dynamic nodes:', [...dynamicPageNodeIds])
console.log('Classification reasons:', reasons)

```

## Integration with the Rendering Pipeline

During the publish process in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts), the renderer first calls `findDynamicNodeIds` to obtain the complete set of hole IDs. As the tree-walking render function processes each node, it checks if the current node ID exists in that set:

```typescript
function renderNode(nodeId: string) {
  if (dynamicIds.has(nodeId)) {
    return `<instatic-hole data-node-id="${nodeId}"></instatic-hole>`
  }
  // ...proceed with static markup generation...
}

```

When a match is found, the renderer emits an `<instatic-hole>` element with a `data-node-id` attribute instead of the node's actual markup. The client-side runtime subsequently fetches the missing fragment on demand when the page is served, completing the lazy-loading cycle.

## Extending the Detection System

The architecture provides two primary extension points for customizing dynamic content detection:

**Custom Binding Sources**: Developers can extend `isBindingSourceRequestDependent` to recognize additional request-dependent sources beyond the built-in `route.query` and related fields. This function serves as a single, deliberate extensibility point for plugins.

**Dynamic Module Registration**: Modules can opt-in to dynamic detection by setting `dynamic: true` in their registry definition. This declarative approach ensures the detector automatically includes them without requiring custom traversal logic.

## Summary

- Instatic's **auto-detection for hole lazy-loading** centers on the `findDynamicNodeIds` function in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts).
- Four ordered classification rules determine dynamism: explicit module flags, request-dependent bindings, request-dependent loop sources, and recursive visual component analysis.
- A **two-pass traversal** promotes entire loops to holes when their children are dynamic, preventing hole proliferation.
- The **public API** provides both production (`findDynamicNodeIds`) and diagnostic (`findDynamicNodesWithReasons`) entrypoints.
- The **rendering pipeline** uses these IDs to emit `<instatic-hole>` placeholders, enabling client-side lazy-loading of request-dependent content.

## Frequently Asked Questions

### How does Instatic distinguish between static and dynamic content during publishing?

Instatic applies the `classifyNode` function from [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) to every node in the page-tree. This function checks four specific rules: whether the module is explicitly marked dynamic, whether bindings or string tokens reference request-dependent sources, whether loops iterate over request-dependent data, and whether referenced visual components contain dynamic descendants. Nodes matching any rule are added to the dynamic set and rendered as holes.

### What is the purpose of the `<instatic-hole>` placeholder?

The `<instatic-hole>` element serves as a server-side marker indicating where request-dependent content will be injected. During Phase A (build time), the publisher emits this placeholder instead of the actual component markup. When a visitor requests the page (Phase C), the runtime identifies these holes and fetches the missing fragments, enabling **hole lazy-loading** without blocking the initial static shell delivery.

### Can plugins extend which binding sources are considered request-dependent?

Yes. The `isBindingSourceRequestDependent` function is designed as a single extensibility point. While the core system recognizes built-in sources like `route.query`, plugins can extend this function to recognize custom request-dependent sources. This allows the auto-detection rules to automatically flag nodes using these custom bindings without modifying the core classification logic.

### Why does the detection system promote entire loops to holes instead of individual items?

The pre-pass traversal (lines 107-122 in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts)) implements **Rule 3.5** specifically to address performance concerns. If any descendant of a loop body is dynamic, the entire `base.loop` node is promoted to a single hole and the inner dynamic IDs are suppressed. This prevents emitting numerous small `<instatic-hole>` elements for each item in a dynamic list, reducing HTML size and client-side processing overhead (documented as issue ISS-021 in the codebase).