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

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

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.

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.

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 and served via 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:

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

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 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 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 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 coordinates the bake and cache clearing, while 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 completes successfully. Because the LRU cache in 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 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.

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 →