How Templates and Layouts Work in Instatic: The Template Chain Architecture

In Instatic, templates and layouts work through a hierarchical template chain system where global layouts marked as "everywhere" wrap around specific page templates, and the composeTemplateChain function merges them into a unified static page tree at publish time.

The Instatic static site generator (CoreBunch/Instatic) implements a unique template composition system that separates layout wrappers from content templates. Understanding how templates and layouts work in Instatic requires examining the template chain resolution and node tree composition that happens during the build process.

Core Concepts of the Instatic Template System

Template Definitions

In src/core/templates/templatePreviewData.ts, templates are defined as JSON-like declarations that describe pages through slugs, root nodes, and metadata. These definitions serve as the foundation for both layouts and regular page templates.

The Everywhere Layout

The global layout system centers on the everywhere function found in src/core/templates/templateMatching.ts. When you call everywhere('layout'), you register a template whose kind is "layout" into a special slot that applies to every route. This layout typically contains a body node with an outlet where inner content is inserted.

Template Chain Resolution

When a request hits the server, resolveTemplateChain(site, requestInfo) (also in src/core/templates/templateMatching.ts) walks the site's template configuration to build an ordered array. The chain always starts with the everywhere layout (if configured), followed by any content-type-specific layouts, and ends with the target page or entry template.

Dynamic Bindings

Scoped data flow is handled in src/core/templates/dynamicBindings.ts. These bindings (like {{page.title}}) flow from the layout scope down to inner pages, allowing outer wrappers to reference page-specific metadata while maintaining the hierarchical structure.

How Layout Composition Works

The merging process occurs in composeTemplateChain, which is tested in src/core/templates/templateCompose.test.ts. This function performs three critical steps:

  1. Starts with the outermost layout's root node
  2. Locates the layout's outlet node (the insertion point marked as type: 'slot.outlet')
  3. Replaces the outlet recursively with the next template's tree

If a layout lacks an outlet node, the page body attaches directly under the layout's body node. This allows for "plain" layouts that only provide CSS or headers without content injection.

Layout vs. Page Templates

Layout templates contain slug: 'layout' (or custom identifiers) and are registered via everywhere() or forPosts() in the site configuration. Their node trees include structural elements like headers, footers, and crucially, a slot.outlet node where content merges.

Page templates are regular page definitions that supply actual content. When composed, their node trees splice into the layout's outlet, creating the final document structure.

Implementation Example

Define a global layout in src/site/layout.ts:

export const layout = (): Page => ({
  id: 'layout',
  slug: 'layout',
  title: 'Site Layout',
  rootNodeId: 'L_body',
  nodes: {
    L_body: {
      type: 'base.container',
      tag: 'div',
      children: [{ type: 'slot.outlet', name: 'default' }],
    },
  },
});

Register the layout in src/site/site.ts:

import { layout } from './layout';
import { everywhere, forPosts } from '@core/templates';

export const site = site([
  everywhere('layout'),
  forPosts('postLayout'),
]);

Create a page template in src/site/pages/about.ts:

export const aboutPage = (): Page => ({
  id: 'about',
  slug: 'about',
  title: 'About Us',
  rootNodeId: 'A_body',
  nodes: {
    A_body: {
      type: 'base.container',
      tag: 'section',
      children: [{ type: 'base.text', text: 'Welcome!' }],
    },
  },
});

When a user visits /about, the system executes:

const chain = resolveTemplateChain(site, { kind: 'page' });
const mergedTree = composeTemplateChain(chain, { kind: 'page', page: aboutPage });

The resulting mergedTree contains the layout's wrapper with the "About Us" content spliced into the outlet, ready for static HTML generation.

Summary

  • Template chains are resolved per request via resolveTemplateChain in src/core/templates/templateMatching.ts, creating an ordered array from global layouts to specific pages
  • Layout composition occurs through composeTemplateChain, which merges node trees by replacing outlet nodes with content
  • Everywhere layouts are registered using the everywhere() function and serve as the root wrapper for all pages
  • Static generation happens at publish time, producing fully static HTML without runtime React overhead
  • Dynamic bindings support scoped data flow from layouts to nested content through src/core/templates/dynamicBindings.ts

Frequently Asked Questions

What is the difference between a layout and a template in Instatic?

In Instatic, a layout is a special template marked with kind: "layout" that wraps around other content. While regular templates define specific page content, layouts contain outlet nodes where that content is inserted. Layouts are registered globally via everywhere() or for specific content types via forPosts(), whereas page templates are individual entries in the template chain.

How does the everywhere layout function work?

The everywhere function in src/core/templates/templateMatching.ts places a layout into the global template slot, ensuring it applies to every route. When resolveTemplateChain runs, it always includes the everywhere layout as the first element in the returned array, making it the outermost wrapper in the final composition.

Where does template composition happen in the codebase?

Template composition is implemented in composeTemplateChain, which is tested in src/core/templates/templateCompose.test.ts. This function takes the resolved template chain and merges the node trees by finding outlet nodes and splicing in the next template's content recursively until a single unified tree remains.

Are Instatic layouts rendered at runtime or build time?

Instatic layouts are composed entirely at build time. The composeTemplateChain function merges layout and page trees during the publishing process, resulting in static HTML that requires no runtime JavaScript for layout rendering. This architecture eliminates client-side hydration overhead for layout structures.

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 →