# Instatic Three-Layer Publishing Architecture: Static Bake, LRU Cache, and Dynamic Holes Explained

> Understand Instatic's three-layer publishing architecture static bake LRU cache and dynamic holes for instant pre-rendered HTML with personalized content.

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

---

**Instatic's publishing pipeline combines atomic static file generation, an in-memory LRU cache, and lazy-loaded dynamic holes to deliver pre-rendered HTML instantly while supporting personalized content fragments.**

The CoreBunch/Instatic repository implements a hybrid rendering strategy that bridges traditional static site generation with runtime personalization. This Instatic three-layer publishing architecture ensures maximum cacheability for public content while preserving the flexibility to inject user-specific data after the initial page load.

## Layer A: Static Bake for Atomic Deployments

The foundation of Instatic's architecture is the **static bake** process, implemented in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts). During a publish operation, the system pre-renders every possible page route into plain HTML files stored at `uploads/published/current/<route>.html`.

The bake process uses atomic deployment patterns to eliminate downtime. The `writeArtefact` function writes files to an inactive *slot* directory first, then the `swapSlot` function moves a symlink atomically to point to the new slot. This ensures that visitors never see a partially deployed site or broken asset references during the publish process.

```typescript
import { publishSite } from '@/server/publish/publishSite';

// Triggers the static bake and atomic slot swap
await publishSite({ siteId: currentSite.id });

```

## Layer B: LRU Render Cache for Request-Dependent Routes

For routes that cannot be fully pre-rendered—such as those depending on query strings or user-specific data—Instatic employs an in-memory **Least-Recently-Used (LRU) cache** defined in [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts).

The cache keys entries by a tuple of `(urlPath, queryString, publishVersion)`. Every successful publish increments `publishVersion`, which automatically invalidates the entire cache and prevents stale content from being served after a new deployment. When a request hits a dynamic route, the system checks `getFromCache` before attempting expensive re-rendering.

```typescript
import { getFromCache } from '@/server/publish/renderCache';

const cached = getFromCache(urlPath, queryString, publishVersion);
if (cached) {
  return new Response(cached, { headers: { 'content-type': 'text/html' } });
}

```

## Layer C: Dynamic Holes for Runtime Personalization

The **dynamic holes** layer, coordinated by [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts) and served via [`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts), handles content that must be generated per-request. During the static bake, the page tree walker inspects each node; if a node depends on request context, it emits a lightweight placeholder instead of HTML:

```html
<instatic-hole data-node-id="abc-123"></instatic-hole>

```

At runtime, a minimal IntersectionObserver-based client script (approximately 668 bytes) watches for these placeholders to enter the viewport. When visible, the browser fetches the actual fragment from `/_instatic/hole/<nodeId>`, allowing the server to render the node with full request context and return the personalized markup.

```typescript
useEffect(() => {
  const observer = new IntersectionObserver(async (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        const resp = await fetch(`/_instatic/hole/${nodeId}`);
        const html = await resp.text();
        entry.target.innerHTML = html;
        observer.unobserve(entry.target);
      }
    }
  });
  
  observer.observe(document.querySelector(`[data-node-id="${nodeId}"]`)!);
  return () => observer.disconnect();
}, []);

```

## How the Three Layers Work Together

The [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) orchestrator ties these layers into a cohesive pipeline:

1. **Pre-render Pass**: The static bake writes all fully static pages to disk using `writeArtefact`, while the walker in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts) marks request-dependent nodes for hole injection.

2. **Cache Priming**: After the atomic slot swap completes, the `publishVersion` increments globally, clearing the LRU cache in [`renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/renderCache.ts) to ensure fresh content.

3. **Request Resolution**: Incoming requests first check for static files on disk. If none exist, the system queries the LRU cache using the current `publishVersion`. Cache misses trigger hole-based rendering for dynamic fragments.

4. **Client Hydration**: The browser receives lightweight static HTML immediately, then lazily fetches personalized hole content only when the user scrolls those elements into view.

This architecture delivers the performance benefits of static site generation—serving plain HTML files directly from disk—while maintaining the flexibility to personalize content through strategic caching and on-demand fragment loading.

## Summary

- **Static Bake**: Pre-renders pages to `uploads/published/current/` and deploys atomically via `swapSlot` to eliminate downtime.
- **LRU Cache**: Stores rendered dynamic pages keyed by URL, query string, and `publishVersion`, with automatic invalidation on every publish.
- **Dynamic Holes**: Replaces request-dependent nodes with placeholders during bake, then fetches actual content via `/_instatic/hole/<nodeId>` using a lightweight IntersectionObserver.
- **Integration**: [`publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/publishSite.ts) coordinates the bake and cache clearing, while [`publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/publicRouter.ts) serves both static assets and hole endpoints.

## Frequently Asked Questions

### How does Instatic handle cache invalidation during deployments?

Instatic increments a global `publishVersion` counter every time [`publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/publishSite.ts) completes successfully. Because the LRU cache in [`renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/renderCache.ts) includes this version number in its cache keys, the entire cache effectively invalidates automatically when a new version deploys, ensuring no stale fragments persist across publishes.

### What happens if a dynamic hole fails to load?

If the fetch to `/_instatic/hole/<nodeId>` fails or returns an error, the placeholder element remains empty or retains its loading state. The client-side IntersectionObserver disconnects after the first fetch attempt regardless of success, preventing infinite retry loops and preserving page performance.

### Can static and dynamic content coexist on the same page?

Yes. The walker in [`dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/dynamicDetection.ts) evaluates each node in the page tree individually during the bake process. Static nodes render fully to HTML, while dynamic nodes emit `<instatic-hole>` placeholders, allowing a single page to serve cached public content alongside personalized fragments that hydrate at runtime.

### Why does Instatic use an LRU cache instead of serving all content statically?

The LRU cache bridges the gap between fully static content (which cannot vary by user or query parameters) and fully dynamic server-side rendering (which is computationally expensive). By caching rendered output keyed by request parameters, Instatic avoids re-rendering identical requests while still supporting personalization through the dynamic holes mechanism for content that truly varies per visitor.