# How the Instatic Three-Tier Publishing Pipeline Balances Speed and Dynamic Content

> Discover Instatic's three-tier publishing pipeline: disk baking, LRU caching, and dynamic rendering. Serve fast pre-built HTML and hydrate dynamic content client-side for optimized performance.

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

---

**Instatic’s three-tier publishing pipeline separates disk baking, in-memory LRU caching, and dynamic island rendering to serve pre-built HTML from the filesystem while hydrating per-visitor content client-side.**

The CoreBunch/Instatic repository implements a hybrid static site architecture that eliminates the traditional trade-off between static performance and dynamic personalization. By routing requests through three distinct layers—disk artefacts, memory caches, and on-demand fragments—the Instatic three-tier publishing pipeline guarantees that static pages never hit the server while still supporting real-time dynamic content.

## Architecture Overview

The pipeline processes every page through a progressive fallback system:

- **Layer A (Disk Bake):** Pre-rendered HTML served directly from the filesystem using atomic deployments.
- **Layer B (In-Memory LRU):** Cached responses for URLs with query parameters, keyed by canonical request signatures.
- **Layer C (Dynamic Islands):** Client-side hydration of `<instatic-hole>` fragments for per-visitor data.

This separation ensures maximum cache efficiency while preserving the ability to inject personalized content without rebuilding entire pages.

## Layer A: Disk Baking and Atomic Deployment

During a full publish operation, the core `publishPage` function in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts) walks the page tree, collects deduplicated CSS via [`src/core/publisher/cssCollector.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/cssCollector.ts), and determines which nodes are static versus dynamic using [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts).

The disk baking layer writes every static page to `uploads/published/current/<route>.html` using a two-slot symlink swap strategy implemented in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts). This atomic update pattern ensures zero-downtime deployments—readers always see a complete version of the site even during file writes.

Pages containing dynamic nodes receive a static shell with `<instatic-hole>` placeholders rather than full content, allowing the build to proceed without blocking on per-visitor data.

## Layer B: In-Memory LRU Caching

When a request arrives with query parameters that affect rendering, Instatic falls back to an in-memory LRU cache managed by [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts). The cache stores full HTML responses keyed by the tuple `(urlPath, canonicalQuery)`, ensuring that identical parameter combinations reuse rendered output without recomputation.

The cache respects versioning through [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts), which maintains a monotonic `publishVersion`. When a new publish completes, the version bumps automatically, evicting stale entries and preventing cache pollution across deployments.

## Layer C: Dynamic Islands for Per-Visitor Content

Pages marked as dynamic during the build phase emit `<instatic-hole>` tags via [`src/core/publisher/renderNode.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/renderNode.ts) rather than final HTML. The client-side runtime in [`server/publish/holeRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/holeRuntime.ts)—a ~1.1 KB IntersectionObserver-based script—detects these placeholders and lazily fetches fragments from the `/_instatic/hole/<nodeId>` endpoint defined in [`server/handlers/cms/hole.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/hole.ts).

This endpoint re-renders the specific node on-demand, bypassing both disk and memory caches to deliver truly per-visitor content. Because only the dynamic fragment executes server-side, the initial HTML remains lightweight and cacheable.

## Rendering Pipeline Implementation

The publishing process begins with the `publishPage` entry point, which orchestrates the entire flow:

```typescript
import { publishPage } from "./src/core/publisher/render.ts";

const { html, filename } = await publishPage(pageTree, siteDoc, moduleRegistry);
// html contains the full document; filename maps to uploads/published/current/

```

During tree traversal, `findDynamicNodeIds` in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) classifies nodes:

```typescript
import { findDynamicNodeIds } from "./src/core/publisher/dynamicDetection.ts";

const dynamicIds = findDynamicNodeIds(pageTree, siteDoc, registry);
if (dynamicIds.has(nodeId)) {
  // renderNode emits <instatic-hole> for this node (Layer C)
}

```

## Request Routing Through All Three Layers

The public router in [`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts) implements the three-tier resolution strategy:

```typescript
import { renderPublicResolution } from "./server/publish/publicRouter.ts";

export async function handleRequest(req) {
  const response = await renderPublicResolution(req, {
    // Router execution flow:
    // 1. Attempt disk artefact retrieval (Layer A)
    // 2. Fall back to LRU cache for parameterized URLs (Layer B)  
    // 3. Render dynamic holes on demand (Layer C)
  });
  return response;
}

```

This hierarchical check ensures that static content serves instantly from disk, cached variants hit memory, and only personalized fragments trigger server-side rendering.

## Summary

- **Disk baking** in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) provides atomic, zero-downtime deployments through symlink swaps, writing static HTML to `uploads/published/current/<route>.html`.
- **In-memory LRU caching** via [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts) accelerates parameterized requests using versioned cache keys keyed by `(urlPath, canonicalQuery)`, with invalidation handled by [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts).
- **Dynamic islands** using [`server/publish/holeRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/holeRuntime.ts) and [`server/handlers/cms/hole.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/hole.ts) enable per-visitor content without sacrificing initial page performance, hydrating `<instatic-hole>` fragments on demand.
- **Dynamic detection** in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) determines at build time which nodes require runtime rendering versus static baking.

## Frequently Asked Questions

### How does Instatic handle cache invalidation during new publishes?

The `publishVersion` counter in [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts) increments atomically with each deployment. The in-memory LRU cache associates every entry with the current version at insertion time, and lookups automatically filter out entries with mismatched versions. This version-based eviction ensures stale content disappears immediately without requiring manual cache flushes.

### What happens if a page contains both static and dynamic content?

During the bake phase in [`src/core/publisher/render.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/render.ts), [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts) analyzes the node tree to identify dynamic subtrees. Static portions render to HTML immediately, while dynamic nodes receive `<instatic-hole>` placeholders. The final baked file contains the complete static shell, and the client-side runtime only hydrates the specific holes marked as dynamic, keeping the initial payload minimal while preserving SEO-friendly static content.

### Where are the baked HTML files stored on disk?

The disk baking layer writes to `uploads/published/current/<route>.html` using the atomic write implementation in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts). The system maintains two slots and performs a symlink swap to switch traffic atomically from the old build to the new one, ensuring readers never encounter partially written files during the publish process.

### Why does the pipeline use an in-memory cache when files already exist on disk?

The disk layer serves only exact URL matches without query parameters. When requests include canonical query strings that affect rendering—such as filtered views or search parameters—[`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts) stores the computed HTML keyed by both the path and normalized parameters. This prevents re-rendering identical parameterized requests while keeping the disk layer simple and filesystem-backed for the common case of clean URLs.