How Instatic Templates Handle Layouts with Outlets and Content Flow
Instatic templates define reusable page layouts through a single base.outlet node that acts as the insertion point, merging template chains via resolveTemplateChain and composeTemplateChain to stream content into a composed tree for publishing.
In the CoreBunch/Instatic static site generator, templates are specialized pages that provide chrome and structure for other content. According to the source code, the system treats layouts as normal page trees containing exactly one base.outlet node where matched content is spliced, creating a seamless flow from outer global wrappers to inner post-type-specific layouts.
Template Resolution and the Matching Pipeline
The template system resolves applicable layouts in three distinct stages: matching, composition, and publishing. This pipeline ensures that content flows from the most general layout to the most specific, terminating with the actual page or entry content.
Resolving the Template Chain with resolveTemplateChain
The entry point for layout selection is resolveTemplateChain, implemented in src/core/templates/templateMatching.ts. This function evaluates the site’s pages against a route context to determine which templates apply.
The matcher evaluates two breadth levels:
everywhere– The outermost global layout that applies to all routespostTypes– The innermost layout targeting specific content types
For each level, the system selects the highest-priority template, using document order as a tiebreaker. The function returns an ordered array running outer → inner (Page[]), establishing the sequence in which layouts will wrap the content.
Composing Layouts with composeTemplateChain
Once the chain is resolved, composeTemplateChain in src/core/templates/templateCompose.ts performs the actual merging. The composer filters out any template lacking a base.outlet node, as these cannot accept content.
The composition process works from the inside out:
- Starting with the innermost template, it splices the terminal page’s node tree into the outlet using
spliceIntoOutlet - Each subsequent outer template wraps the current tree by splicing it into its own outlet
- The merger re-keys node IDs, removes the outlet node itself, and reindexes parent relationships via
reindexNodeParents
Because the outlet node is removed during this process, it never appears in the final rendered output—only its placeholder children are replaced by the actual content.
The base.outlet Node and Content Flow Control
The outlet system relies on helpers centralized in src/core/templates/outlet.ts to maintain strict invariants:
firstOutletId– Returns the ID of the first outlet found, ornullif absenttreeHasOutlet– Reports whether a document contains any outlet nodesubtreeHasOutlet– Guards against duplicating subtrees that would introduce a second outlet
These utilities ensure that at most one outlet exists after composition. This guarantee allows the editor UI to safely expose a single Content Outlet tile in the module picker, as documented in docs/features/templates.md and implemented in src/admin/pages/site/module-picker/moduleInserterModel.ts. The picker disables the tile for non-template pages or for templates that already contain an outlet.
Publishing the Final Composed Tree
After composition, the merged tree is passed to the publisher, which treats it identically to any regular page tree. The absence of the base.outlet node in the final structure means the rendered HTML consists solely of the layout’s chrome—headers, footers, and sidebars—plus the page-specific body content that flowed through the outlet chain.
Practical Implementation Examples
The following examples demonstrate the complete workflow from matching to publishing:
// Resolve the template chain for a public route.
// For a post entry with slug "my-post", the context specifies the entry kind and table.
import { resolveTemplateChain } from '@core/templates';
import { site } from './exampleSite';
const ctx = { kind: 'entry', tableSlug: 'posts' } as const;
const chain = resolveTemplateChain(site, ctx);
// chain → [globalLayoutPage, postTypeLayoutPage]
// Compose the chain with the terminal page holding the actual content.
import { composeTemplateChain } from '@core/templates';
import { aboutPage } from './pages';
const merged = composeTemplateChain(chain, { kind: 'page', page: aboutPage });
// merged.nodes contains the layout + content, with the outlet replaced
// Publish the merged page (the publisher treats it as a normal page).
import { publishPage } from '@core/publisher';
await publishPage(merged);
Summary
- Templates are pages that define layouts through a single
base.outletnode acting as a content insertion point resolveTemplateChainselects applicable layouts by evaluatingeverywhereandpostTypesbreadth levels, returning an ordered array from outer to innercomposeTemplateChainmerges templates by splicing content into outlets, re-keying node IDs, and removing the outlet node so it never reaches the final HTML- Outlet helpers in
src/core/templates/outlet.tsenforce the invariant of at most one outlet per composed tree, enabling UI gating in the module picker - The publisher receives a clean, composed page tree identical to standard pages, with content flowed through the entire template hierarchy
Frequently Asked Questions
What happens if a template in the chain lacks a base.outlet?
The composer ignores templates without a base.outlet node. According to the logic in src/core/templates/templateCompose.ts, these templates are filtered out silently rather than throwing an error, as they cannot serve as viable containers for spliced content.
How does Instatic prevent multiple outlets in the final tree?
The system uses subtreeHasOutlet from src/core/templates/outlet.ts to guard against operations that would duplicate a subtree containing an outlet. This ensures the composed tree maintains at most one outlet, which is then removed during the splice operation, leaving no outlet nodes in the published output.
Can templates nest indefinitely in the chain?
While there is no explicit depth limit in resolveTemplateChain, the composition order is strictly outer-to-inner based on the array returned by the matcher. Each template wraps the result of the previous splice, so practical limits depend on the number of matching templates found for the everywhere and postTypes levels.
How does the editor know when to show the Content Outlet tile?
The UI logic in src/admin/pages/site/module-picker/moduleInserterModel.ts checks the outlet invariant using helpers like treeHasOutlet. The Content Outlet tile is disabled for non-template pages and for templates that already contain an outlet, ensuring users cannot create invalid multi-outlet configurations.
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 →