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

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 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 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, 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 substitutes each dynamic node with a lightweight custom element:

// 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—registers an IntersectionObserver that fetches actual fragments from /_instatic/hole/<nodeId> when placeholders enter the viewport.

Code Examples

Finding Dynamic Node IDs

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

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 Implements findDynamicNodeIds and the four detection rules
src/core/publisher/render.ts Walks tree, substitutes dynamic nodes with <instatic-hole>
src/core/publisher/holeLoader.ts Client-side IntersectionObserver for lazy fragment fetching
src/core/page-tree/treeSchema.ts Defines NodeTree shape used throughout detection
src/core/publisher/dynamicDetection.test.ts Unit tests verifying detection logic

Summary

  • findDynamicNodeIds in 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 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.

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 →