How Dynamic Detection Works in Instatic’s Publishing Pipeline

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. 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) 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, 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, the publishPage function invokes detection at the pipeline start (line 98):

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

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

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:

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, 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.

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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →