| layout | docs |
|---|---|
| docsOrder | 40 |
| handlebars | false |
Layouts wrap page content in shared HTML and can contribute variables, data subscriptions, styles, and browser code. Use a single root layout for a simple site, or declare parent layouts to share structure across sections. For a complete working example, see Compose nested layouts.
[[toc]]
Layouts are "outer page templates" that pages get rendered into.
You can define as many as you want, and they can live anywhere in the src directory.
Layouts are named ${layout-name}.layout.ts where ${layout-name} becomes the name of the layout.
Layouts should have a unique name, and layouts with duplicate names result in a build error.
Note
Wherever you see .layout.ts being used, you can also use .layout.js.
Type checking is supported in both file types.
See Supported file types for all available extensions.
Example layout file names:
src/layouts/root.layout.ts # this layout is referenced as 'root'
src/other-layouts/article.layout.ts # this layout is referenced as 'article'DOMStack ships a default root layout, so defining one in your src directory is optional, though recommended.
Owning your own root layout will make DOMStack updates easier, and give you more control over your site.
All pages have a layout variable that defaults to root.
If you set the layout variable to a different name, pages will build with a layout matching the name you set to that variable.
The following markdown page would be rendered using the article layout.
---
layout: 'article'
title: 'My Article Title'
---
Thanks for reading my articleA page referencing a layout name that doesn't have a matching layout file will result in a build error. Filenames determine layout names, but nesting is an explicit module declaration, not a directory or import convention.
DOMStack recognizes these exports from a layout module:
| Export | Required | Contract |
|---|---|---|
default |
Yes | A synchronous or asynchronous layout render function. |
vars |
No | An object, or a sync/async function returning an object, providing layout defaults. |
parentLayout |
No | A non-empty string naming the immediate outer layout; see Declaring nested layouts. |
pageOutputs |
No | A build-only function returning extra files for each source page, such as Markdown downloads or JSON metadata. |
Declare a parent with a named parentLayout export in the child layout module:
// src/layouts/article.layout.ts
import type { LayoutFunction } from '@domstack/static/types.js'
export const parentLayout = 'root'
const articleLayout: LayoutFunction<Record<string, never>, string, string> = ({ children }) => {
return `<article>${children}</article>`
}
export default articleLayoutparentLayout is a layout name, not a file path or imported function.
For example, 'root' resolves the discovered root.layout.ts or root.layout.js, wherever it lives under src, or DOMStack's bundled root when no custom root exists.
Names are matched exactly, using the same filename-derived names as the page's layout variable.
Omit parentLayout (or export undefined) when the layout has no parent; DOMStack does not automatically wrap a selected non-root layout in root.
DOMStack renders the page, passes its result to article, then passes that result to root: root(article(page())).
Each parent can declare another parent, forming a chain that ends at a layout without parentLayout.
Missing parents and cycles, including a layout naming itself, fail the build.
Every render step is awaited, and each parent receives its immediate child's return value as children without intermediate string conversion.
The outermost result is converted to a string for HTML output.
All layouts receive the same final resolved page vars, metadata, and asset lists.
Layout defaults merge outermost-to-innermost before page overrides, and ancestor CSS/client entries are included automatically between global and page assets.
Watch mode tracks the resolved chain and each layout's static imports for source-backed and generated pages, updating those relationships after successful rebuilds.
Each layout can also declare its own global-data subscriptions through vars.dataDeps.
DOMStack passes only those declared keys to that layout's data argument; a child does not receive its parent's data or need to repeat its declarations.
For rebuilds, the page depends on the union of its own subscriptions and every layout's subscriptions in the declared chain.
See Data subscriptions in nested layouts for typed declarations and examples.
See Compose nested layouts for a complete example and asset guidance.
Export pageOutputs from a layout to produce extra files for each source-backed page that uses it.
This is the same export used by page modules and vars companions, and it receives { page, vars, data }.
The function runs for each page, and that page owns the returned files.
For example, a documentation layout can publish a Markdown download alongside each rendered page:
// src/docs.layout.js
export default ({ children }) => children
export async function pageOutputs ({ page, vars }) {
if (page.type !== 'md' || vars.rawExport === false) return []
return {
outputName: page.outputName.replace(/\.html$/, '.source.md'),
content: await page.readMarkdownContent(),
}
}The filename is relative to the current page's output directory, not the layout directory.
Using the page's HTML filename helps keep destinations unique when several loose Markdown pages share a directory.
Exact duplicate destinations produce best-effort build warnings, not an override contract; avoid sharing output paths between pages or hooks.
Set rawExport: false in a page's frontmatter or vars to opt out of this layout's Markdown download.
Returning [] from a page hook does not suppress layout files; the layout itself must check the opt-out variable.
Nested hooks run outermost layout → innermost layout → selected page-level hook, and their files are additive.
If a JS/TS page module and its vars companion both export pageOutputs, the page module's hook wins with a warning; layout hooks still run.
Each layout hook receives the fully resolved page vars and only that layout renderer's vars.dataDeps subscriptions in data.
Declare data needed by the hook in the same subscriptions used by the layout render function.
Hooks may return a { outputName, content } record, an array of records, or an async iterable of records, directly or through a promise.
DOMStack validates and processes each file before requesting the next record, writing it or retaining an unchanged file during watch rebuilds.
A later hook runs only after the preceding hook's files have been processed.
Files are written directly, without transactions or rollback; if a later record or hook fails, earlier writes remain in the destination.
Watch mode tracks these files for cleanup after a successful rebuild or source removal.
Support for generated *.pages.* pages is deferred; they skip these hooks, including inherited layout hooks.
See Page outputs for the complete arguments, public types, path rules, and watch behavior.
Layouts may also export an optional vars variable provider containing defaults for pages that use the layout:
export const vars = {
showSidebar: true,
pageType: 'article',
}Layout vars are merged into the resolved variable cascade for pages using that layout. Precedence is:
page/frontmatter vars > page.vars.* > inner layout vars > outer layout vars > global.vars > domstack defaultsThis makes layout vars useful for section-wide defaults while still letting individual pages override them.
A layout's default export is an async or sync function that wraps its children in an outer template.
With nested layouts, children is the result of the immediately inner layout, or the page itself for the innermost layout.
It is always passed a single object argument with the following entries.
See Page data and introspection for details about page, and Global data for data:
vars: The resolved page variable cascade, including domstack defaults, global vars, layout vars, page vars, and page builder vars/frontmatter. Pages can customize layouts by overriding global or layout defaults.data: Only the top-level global-data keys declared by this layout throughvars.dataDeps.scripts: array of paths that should be included onto the page in a script tag src with typemodule.styles: array of paths that should be included onto the page in alink rel="stylesheet"tag with thehrefpointing to the paths in the array.children: The immediate child's render result: the page's content for the innermost layout, or the next inner layout's return value for a parent. Markdown and HTML pages return strings; TypeScript pages and nested layouts may return other values.page: An object with metadata and other facts about the current page being rendered into the template.
The default root.layout.ts is featured below, and is implemented with fragtml, though it could just be done with a template literal or any other template system that runs in Node.js.
See the fragtml docs for escaping, raw HTML, rendering, and fragment usage.
root.layout.ts can live anywhere in the src directory.
import { html, raw, render } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'
import type { LayoutFunction } from '@domstack/static/types.js'
type RootLayoutVars = {
title: string,
siteName: string,
defaultStyle: boolean,
basePath?: string
}
export const vars = {
defaultStyle: true,
}
const defaultRootLayout: LayoutFunction<RootLayoutVars, string | HtmlResult, string> = ({
vars: {
title,
siteName = 'Domstack',
basePath,
/* defaultStyle = true Set this to false in global or page vars to disable the default style in the default layout */
},
scripts,
styles,
children,
data,
page,
}) => {
return render(html`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>${title ? `${title}` : ''}${title && siteName ? ' | ' : ''}${siteName}</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="color-scheme" content="light dark" />
${scripts
? scripts.map(script => html`<script type="module" src="${script.startsWith('/') ? `${basePath ?? ''}${script}` : script}"></script>`)
: null}
${styles
? styles.map(style => html`<link rel="stylesheet" href="${style.startsWith('/') ? `${basePath ?? ''}${style}` : style}" />`)
: null}
</head>
<body class="safe-area-inset">
<main class="mine-layout app-main">${typeof children === 'string' ? raw(children) : children}</main>
</body>
</html>
`)
}
export default defaultRootLayoutIf your src folder doesn't have a root.layout.ts file somewhere in it, domstack will use the default default.root.layout.js file it ships.
The default root layout includes a special boolean variable called defaultStyle that lets you disable a default page style (provided by mine.css) that it ships with.
You can create a ${layout-name}.layout.css next to any layout file.
While the layout file can live anywhere in src, the layout style must live next to the associated layout file.
/* /layouts/article.layout.css */
.layout-specific-class {
color: blue;
& .button {
color: purple;
}
}
/* This layout style is included in every page rendered with the 'article' layout */Layout styles are loaded on all pages that use that layout directly or through a parentLayout chain.
Layout styles are bundled with esbuild and can bundle relative and npm css using css @import statements.
DOMStack loads stylesheets in this order: optional defaults, global, outermost-to-innermost layouts, then page. Under the normal CSS cascade, later styles take precedence when origin, importance, cascade layer, and specificity are otherwise equal. This lets page styles override layout styles, and inner layout styles override outer layout styles. See Global bundles for optional cascade-layer conventions.
You can create a ${layout-name}.layout.client.ts next to any layout file.
While the layout file can live anywhere in src, the layout client bundles must live next to the associated layout file.
Note
Use ${layout-name}.layout.client.tsx when a layout client bundle contains JSX.
You can also use .jsx.
See Supported file types for all available extensions and .tsx client bundles for JSX configuration.
/* /layouts/article.layout.client.ts */
console.log('I run on every page rendered with the \'article\' layout')
/* This layout client is included in every page rendered with the 'article' layout */Layout client bundles are loaded on all pages that use that layout directly or through a parentLayout chain.
Layout client bundles are built with esbuild and can bundle relative and npm modules using ESM import statements.
Layouts can be typed using LayoutFunction<T, U, V, D> where:
Tis the variables typeUis the immediate child's render result, from a page or nested layout (defaults toany)Vis the layout's return type (defaults tostringfor HTML output)Dis the declared global-data shape (defaults toRecord<string, unknown>)
import type { LayoutFunction } from '@domstack/static/types.js'
import type { HtmlResult } from 'fragtml/types.js'
import { html, raw, render } from 'fragtml'
type ArticleLayoutVars = {
title: string
showSidebar: boolean
}
const articleLayout: LayoutFunction<ArticleLayoutVars, string | HtmlResult, string> = ({
vars,
children,
}) => {
return render(html`
<article>
<h1>${vars.title}</h1>
${typeof children === 'string' ? raw(children) : children}
${vars.showSidebar ? html`<aside>Related articles</aside>` : null}
</article>
`)
}
export default articleLayoutDOMStack's bundled default layout uses fragtml because the default template only needs safe string manipulation.
You can eject or replace that layout with any Node-compatible renderer that returns an HTML string.
The previous incumbent for this job was htm/preact with preact-render-to-string.
That is still a good fit when your Node-side pages or layouts produce Preact VNodes, or when you want the same component model on the server and in browser bundles.
If you also want Preact or React in browser JSX/TSX bundles, configure that separately as described in .tsx.
npm install htm preact preact-render-to-string/**
* @import { LayoutFunction } from '@domstack/static/types.js'
* @import { VNode } from 'preact'
*/
import { html } from 'htm/preact'
import { render } from 'preact-render-to-string'
/** @type {LayoutFunction<Record<string, any>, string | VNode, string>} */
export default function rootLayout ({ children, vars, scripts, styles }) {
return `<!DOCTYPE html>
${render(html`<html lang=${vars.lang ?? 'en'}>
<head>
<title>${vars.title}</title>
${styles?.map(style => html`<link rel="stylesheet" href=${style} />`)}
${scripts?.map(script => html`<script type="module" src=${script}></script>`)}
</head>
<body>
${typeof children === 'string'
? html`<main dangerouslySetInnerHTML=${{ __html: children }} />`
: html`<main>${children}</main>`}
</body>
</html>`)}`
}preact-render-to-string works, but it builds a virtual DOM tree just to serialize layout HTML.
For layouts that mostly combine strings and already-rendered page content, async-htm-to-string keeps the familiar HTM tagged-template style while rendering directly to strings.
That can be a better-performing and more direct tool for server-only layout templates.
You can still use Preact for browser-side components and use async-htm-to-string for Node-side layout rendering.
npm install async-htm-to-string/**
* @import { LayoutFunction } from '@domstack/static/types.js'
*/
import { html, rawHtml } from 'async-htm-to-string'
/** @type {LayoutFunction<Record<string, any>, string, Promise<string>>} */
export default async function rootLayout ({ children, vars, scripts, styles }) {
return await html`<!DOCTYPE html>
<html lang="${vars.lang ?? 'en'}">
<head>
<title>${vars.title}</title>
${styles?.map(style => html`<link rel="stylesheet" href="${style}" />`)}
${scripts?.map(script => html`<script type="module" src="${script}"></script>`)}
</head>
<body>
<main>${rawHtml(children)}</main>
</body>
</html>`
}Key differences from htm/preact and DOMStack's fragtml default:
- Attribute names are standard HTML.
Use
classandforrather than React aliases likeclassNameandhtmlFor, whichasync-htm-to-stringwill output literally with no warning. For attributes liketabindex,tabIndexis only a casing preference in HTML, but using standard lowercase keeps templates consistent. - Always
awaitthehtmltag. The tag returns an object that resolves to a string asynchronously. If you return it withoutawaitfrom a non-async function, or assign it where a string is expected, you will get[object Object]in the output with no error thrown. Useasync functionandawaitthe result.
Caution
rawHtml() bypasses HTML escaping and is equivalent to setting innerHTML directly.
Only use it with trusted HTML that you generated or sanitized yourself, such as the output of await page.renderInnerPage() or a trusted Markdown renderer.
children passed to a layout can be any type returned by a page function and may contain unsanitized content; always verify its source before passing it to rawHtml().