# How the Instatic Publisher Handles Dynamic Nodes with instatic-hole Placeholders

> Learn how Instatic Publisher manages dynamic nodes using instatic-hole placeholders. Discover how it identifies, swaps, and hydrates content on demand for efficient static site generation.

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

---

**The Instatic publisher identifies dynamic content at build time, swaps it for `<instatic-hole>` placeholders in the static HTML, and hydrates these regions on demand via a specialized runtime that fetches fresh markup from the server.**

The CoreBunch/Instatic repository implements a hybrid static-site architecture that keeps pages cache-friendly while supporting real-time data dependencies. By walking the component tree during the publishing phase, the system isolates nodes that require request-time rendering and defers their execution to a lightweight client-side runtime.

## Detecting Dynamic Nodes in the Build Pipeline

The publisher begins by traversing the entire page tree to classify nodes as static or dynamic. In [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), the detection algorithm evaluates four specific criteria: the presence of a `dynamic: true` prop, usage of Yjs collaborative documents, dependencies on request-specific query parameters, or explicit dynamic flags in component metadata. This logic is thoroughly validated in [`src/__tests__/server/dynamicDetection.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/dynamicDetection.test.ts).

When any of these conditions match, the node is flagged for hole injection rather than static rendering. This determination happens before any HTML generation, ensuring that dynamic boundaries are established early in the pipeline.

## Injecting instatic-hole Placeholders

Once a node is classified as dynamic, the publisher invokes the transformation logic in [`src/core/publisher/holeSubtreeModules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/holeSubtreeModules.ts). This module strips the node’s original children from the static output and replaces them with a minimal custom element: `<instatic-hole data-node-id="unique-id"></instatic-hole>`. The transformation behavior is verified by [`src/__tests__/server/holePlaceholder.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/holePlaceholder.test.ts).

The placeholder carries a unique identifier that maps back to the original component configuration. During the static generation phase, this element is rendered directly into the HTML string, creating a marker that the client runtime can target without executing any component logic upfront.

```typescript
// Simplified placeholder injection from holeSubtreeModules.ts
function createHolePlaceholder(nodeId: string) {
  return `<instatic-hole data-node-id="${nodeId}"></instatic-hole>`;
}

```

## Client-Side Hole Runtime and Lazy Hydration

The published page includes a tiny runtime bundle that initializes when the document loads. As implemented in the test suite at [`src/__tests__/server/holeRuntime.smoke.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/holeRuntime.smoke.test.ts), this runtime creates an `IntersectionObserver` to watch all `<instatic-hole>` elements.

When a placeholder enters the viewport, the runtime dispatches a fetch request to the `/_instatic/hole/:nodeId` endpoint. The server handler defined in [`server/handlers/cms/hole.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/hole.ts) receives this request and delegates to [`server/publish/holeRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/holeRuntime.ts), which reconstructs the rendering context for that specific node only.

```typescript
// Client-side runtime excerpt
const observer = new IntersectionObserver((entries) => {
  entries.forEach(async (entry) => {
    if (entry.isIntersecting) {
      const el = entry.target as HTMLElement;
      const nodeId = el.dataset.nodeId;
      const response = await fetch(`/_instatic/hole/${nodeId}`);
      const html = await response.text();
      el.replaceWith(document.createRange().createContextualFragment(html));
    }
  });
});

document.querySelectorAll('instatic-hole').forEach((hole) => observer.observe(hole));

```

## Server-Side Rendering for Dynamic Fragments

The server-side hole rendering pipeline re-executes only the marked component rather than the entire page. The [`server/publish/holeRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/holeRuntime.ts) module instantiates a minimal rendering environment, applies the original props and data dependencies (including live database queries or Yjs document states), and streams the resulting HTML fragment back to the client.

This approach ensures that dynamic content reflects real-time data without sacrificing the performance benefits of static caching for the page shell.

```typescript
// Server handler excerpt from server/handlers/cms/hole.ts
import { renderHoleNode } from '../publish/holeRuntime.ts';

export async function handleHoleRequest(req: Request) {
  const nodeId = req.params.nodeId;
  const htmlFragment = await renderHoleNode(nodeId);
  return new Response(htmlFragment, {
    headers: { 'Content-Type': 'text/html' }
  });
}

```

## Summary

- **Dynamic detection** occurs in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) using four rules to identify nodes requiring runtime rendering.
- **Placeholder injection** happens via [`src/core/publisher/holeSubtreeModules.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/holeSubtreeModules.ts), which replaces dynamic subtrees with `<instatic-hole>` elements.
- **Client hydration** is managed by a lightweight runtime that uses `IntersectionObserver` to lazy-load content when placeholders become visible.
- **Server re-rendering** is handled by [`server/handlers/cms/hole.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/hole.ts) and [`server/publish/holeRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/holeRuntime.ts), which generate fresh HTML for individual nodes on demand.
- This architecture preserves static-site performance while supporting real-time data dependencies through targeted, on-demand rendering.

## Frequently Asked Questions

### What criteria determine if a node is treated as dynamic?

The system checks for four conditions in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts): explicit `dynamic: true` props, Yjs document usage, request-dependent query parameters, or component-level dynamic flags. If any condition is met, the node is extracted from the static build and handled by the hole runtime.

### Does using instatic-hole placeholders impact SEO?

Search engines receive the static HTML containing the `<instatic-hole>` custom elements. While the placeholder itself is empty, the surrounding content is fully indexed. For critical SEO content, developers should ensure dynamic nodes load quickly or implement server-side rendering fallbacks to minimize empty regions during initial crawls.

### Can the hole runtime fetch behavior be customized?

Yes. The runtime initialization code can be modified before the publisher bundles it into the final page. Developers can adjust the `IntersectionObserver` threshold, add loading states, or implement custom fetch logic by extending the client-side module referenced in [`src/__tests__/server/holePlaceholder.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/holePlaceholder.test.ts).

### What is the performance overhead of dynamic nodes?

The overhead is minimal for the initial page load because dynamic components are excluded from the static HTML payload. The browser only fetches dynamic content when the user scrolls a placeholder into view, and the server re-renders just that single node rather than the entire page, keeping response times low.