How the Instatic Publisher Handles Dynamic Nodes with instatic-hole Placeholders
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, 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.
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. 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.
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.
// 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, 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 receives this request and delegates to server/publish/holeRuntime.ts, which reconstructs the rendering context for that specific node only.
// 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 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.
// 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.tsusing four rules to identify nodes requiring runtime rendering. - Placeholder injection happens via
src/core/publisher/holeSubtreeModules.ts, which replaces dynamic subtrees with<instatic-hole>elements. - Client hydration is managed by a lightweight runtime that uses
IntersectionObserverto lazy-load content when placeholders become visible. - Server re-rendering is handled by
server/handlers/cms/hole.tsandserver/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: 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.
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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →