| layout | docs |
|---|---|
| docsOrder | 90 |
| handlebars | false |
Create output from code when a source file per page is not the right fit. Generated pages use DOMStack's normal page and layout pipeline, while templates write arbitrary files such as feeds, JSON, or text. Both can subscribe to shared values prepared by the data pipeline.
| Use | Choose | Result |
|---|---|---|
| Archives, tag indexes, or HTML redirects with page variables and layouts | *.pages.ts |
One or more DOMStack pages |
| Feeds, sitemaps, JSON, text, or fully controlled output | *.template.ts |
One or more files, without layout wrapping |
| An ordinary page with its own source directory and browser assets | Page files | A source-backed page |
| Markdown downloads, JSON metadata, or other extra files owned by a source-backed page | pageOutputs |
Extra files alongside the page's HTML |
[[toc]]
Generated-pages files create one or more DOMStack pages from a central *.pages.* module.
Unlike templates, generated pages use the normal page and layout pipeline: each definition supplies page variables and children, which DOMStack renders through the selected layout.
Use generated pages for data-driven output such as blog index pages or HTML redirects derived from frontmatter.
Generated-pages files use the *.pages.ts suffix.
Note
Wherever you see *.pages.ts being used, you can also use *.pages.js.
Type checking is supported in both file types.
See Supported file types for all available extensions.
Like variable providers, generated-page factories may be synchronous or asynchronous. Unlike variable providers, they return page definitions and may produce multiple results.
A generated-pages module can default-export:
| Export | Use when |
|---|---|
One GeneratedPageDefinition object |
The module always creates one page |
| An array of definitions | The module always creates a fixed set of pages and needs no build context |
A normal or async function |
Definitions depend on global vars, declared global data, or pages-file metadata |
An async iterable, usually returned by async function* |
Pages are discovered incrementally or the total is not known in advance |
Static objects and arrays do not receive factory parameters.
A null or undefined default export or factory result produces no pages, as does an empty array or async iterable.
Each consumed entry must resolve to a page-definition object; null entries are not skipped.
The runtime consumes arrays with for await, so promise entries are awaited before definition validation.
Export one object when the module always creates a single page:
// src/about.pages.ts
export default {
outputName: 'about/index.html',
vars: { layout: 'root', title: 'About' },
children: '<p>About this site</p>',
}Export an array when the module always creates a fixed set of pages:
// src/legal.pages.ts
export default [
{
outputName: 'terms/index.html',
vars: { layout: 'legal', title: 'Terms' },
children: 'Terms of service',
},
{
outputName: 'privacy/index.html',
vars: { layout: 'legal', title: 'Privacy' },
children: 'Privacy policy',
},
]Export a function when definitions depend on declared global data or shared variables:
// src/tag-indexes.pages.ts
export const dataDeps = ['tagIndex']
export default function tagIndexes ({ data }) {
return Object.entries(data.tagIndex).map(([tag, posts]) => ({
outputName: `tags/${tag}/index.html`,
vars: { layout: 'tag-index', title: `Posts tagged ${tag}`, posts },
}))
}For a complete two-stage factory example, see Generate yearly blog index pages.
Export an async function when creating definitions requires asynchronous work:
// src/team.pages.ts
import { readFile } from 'node:fs/promises'
export default async function teamPages () {
const members = JSON.parse(
await readFile(new URL('./data/team.json', import.meta.url), 'utf8')
)
return members.map(member => ({
outputName: `team/${member.slug}/index.html`,
vars: { layout: 'profile', title: member.name, member },
}))
}Export an async generator when pages should be yielded incrementally:
// src/archive.pages.ts
export const dataDeps = ['blogYears']
export default async function * archivePages ({ data }) {
for (const year of data.blogYears) {
yield {
outputName: `blog/${year}/index.html`,
vars: { layout: 'archive', year },
}
}
}DOMStack consumes generated pages one at a time rather than collecting all definitions before building them.
For each definition, it validates the definition and output path, initializes the page's variables, layouts, and declared data, then renders and writes the HTML before requesting the next definition.
An async generator resumes after yield only once that page has been written, so it can release resources associated with the completed page before preparing the next one.
Arrays and single-object results use the same per-page pipeline, although an array-producing factory necessarily creates its array before returning it.
Definitions marked draft: true are skipped unless drafts are enabled.
Skipped drafts retain their position in source identifiers: after a skipped first definition, the next page is identified as archive.pages.ts#1, not archive.pages.ts#0.
Output paths must be unique across generated pages and must not collide with source-backed pages.
Do not rely on the processing order of sibling factory modules.
A factory, validation, initialization, or render failure stops the active iterator without requesting later definitions.
Async generators can use try/finally to release resources when iteration stops.
Earlier completed pages remain written; generated-page builds are not transactional, and a late failure does not roll back earlier HTML.
Conflict errors still identify both sources even when one page has already been written.
Build results retain output metadata and the owning pages-file path for completed pages, including when the build fails.
In watch mode, failed builds retain both previously owned outputs and any newly written partial outputs without stale-output cleanup. A later successful rebuild removes obsolete outputs, including partial pages from repeated failures or an initially failed watch build. Removing the factory or changing it to return no pages also cleans up its tracked outputs after a successful rebuild. This ownership tracking does not require a public DOMStack manifest and does not remove outputs still owned by sibling factories.
Functions receive one object with:
| Parameter | Contents |
|---|---|
vars |
Default and global vars. |
data |
Only the top-level values named by the module's dataDeps export. |
pagesFile |
Information about the current file. name is the filename without its .pages.* suffix, path is its source-relative directory, and pagesFile contains the underlying file information. |
Factories do not receive raw source or generated PageData collections.
Put page-collection logic in global.data.ts, return a focused serializable value, and subscribe to its key from the factory.
Source-backed pages remain fully initialized before global.data.ts runs, so it can inspect their resolved variables and use their rendering methods subject to the normal data-dependency rules.
Generated pages are not included in that collection, even after earlier yielded pages have been written.
This keeps factories downstream of source discovery without exposing generation order or creating page-generation cycles.
| Field | Behavior |
|---|---|
outputName |
Output path relative to the pages file's directory. It must name a file, must not be absolute or contain .. segments, and cannot end in a path separator. Defaults to <pages-file-name>/index.html. |
vars |
Page-level vars merged with the normal default, global, layout, and builder vars. |
children |
Optional static child content or inline PageFunction rendered before the layout. |
draft |
When true, the page is omitted unless the CLI uses --drafts or a programmatic build uses buildDrafts: true. |
Generated pages use global bundles and layout assets.
They do not have page-local style.css, client.js, or worker entries because they do not have their own source-page directory.
Support for page outputs on generated pages is deferred.
Generated pages skip all pageOutputs hooks, including hooks inherited from layouts.
Use GeneratedPageDefinition<T, U, D> to type an individual definition.
T is the generated page's variables type, U is its children type, which defaults to string, and D is the declared data shape for inline page functions:
// src/terms.pages.ts
import type { GeneratedPageDefinition } from '@domstack/static/types.js'
type LegalPageVars = {
layout: string
title: string
}
const terms: GeneratedPageDefinition<LegalPageVars> = {
outputName: 'terms/index.html',
vars: { layout: 'legal', title: 'Terms' },
children: 'Terms of service',
}
export default termsUse PagesFunction<T, U, V, D> for normal functions, async functions, and async generators:
Tis the variables type added to each generated page.Uis the generated children type (defaults tostring).Vis the default and global vars type received by the factory.Dis the global-data shape declared by the factory.
// src/archive.pages.ts
import type { PagesFunction } from '@domstack/static/types.js'
type ArchiveVars = { layout: string, year: number }
type ArchiveData = { blogYears: number[] }
export const dataDeps = ['blogYears']
const archivePages: PagesFunction<ArchiveVars, string, Record<string, never>, ArchiveData> = async function * ({ data }) {
for (const year of data.blogYears) {
yield {
outputName: `blog/${year}/index.html`,
vars: { layout: 'archive', year },
}
}
}
export default archivePagesFor metadata-driven redirects, see the cookbook recipe Generate redirect pages from page metadata.
For a registered layout chain, use GeneratedPageForLayout<Name, PageVars, PageData, GlobalVars> for a definition or PagesForLayout<Name, PageVars, GlobalVars, FactoryData, PageData> for a factory.
These helpers validate required vars from actual known sources, rather than assuming values exist because a renderer requires them.
PageVars describes only what each definition supplies; inline renderers receive the merged globals, layout defaults, and page vars.
For the registered article layout in the layout guide, which accepts string content and defaults showSidebar, a single definition can be typed as follows:
import type { GeneratedPageForLayout } from '@domstack/static/types.js'
import type globalVars from './global.vars.ts'
type GlobalVars = Awaited<ReturnType<typeof globalVars>>
type ArticleDefinition = GeneratedPageForLayout<
'article',
{ title: string },
Record<string, never>,
GlobalVars
>
const article: ArticleDefinition = {
outputName: 'article/index.html',
vars: { layout: 'article', title: 'Generated article' },
children: ({ vars }) => vars.showSidebar ? '<p>With sidebar</p>' : '<p>No sidebar</p>',
}
export default articleThe actual global provider must supply siteName, as required by the registered root renderer.
The definition supplies neither siteName nor showSidebar, but its inline renderer can access both.
Its vars property is required and must contain the literal layout: 'article', even when a global default would select the same layout.
A missing title, incompatible override, wrong selector, or incompatible children produces a type error.
Factories keep their own inputs separate from each generated page:
import type { DataDeps, PagesForLayout } from '@domstack/static/types.js'
import type globalVars from './global.vars.ts'
type GlobalVars = Awaited<ReturnType<typeof globalVars>>
type FactoryData = { articleTitles: string[] }
type InlineData = { announcement: string }
type SuppliedVars = {
title: string
dataDeps: DataDeps<InlineData>
}
export const dataDeps = ['articleTitles'] satisfies DataDeps<FactoryData>
const articles: PagesForLayout<
'article', SuppliedVars, GlobalVars, FactoryData, InlineData
> = async function * ({ vars: globals, data: factoryData }) {
for (const [index, title] of factoryData.articleTitles.entries()) {
yield {
outputName: `articles/${index}/index.html`,
vars: { layout: 'article', title, dataDeps: ['announcement'] },
children: ({ vars, data }) => {
// vars contains siteName, title, and showSidebar, but not dataDeps.
// data contains announcement, not articleTitles or layout subscriptions.
return `${globals.siteName}: ${vars.title} — ${data.announcement}`
},
}
}
}
export default articlesHere the factory sees only effective global vars and its own subscribed data, not layout defaults or page vars.
The inline page receives its own data contract; FactoryData and PageData default independently and do not inherit from each other.
The global-data provider must produce both declared data keys; supplying type arguments does not create subscriptions.
Escape interpolated content with your template library when producing HTML from untrusted values.
Factories can return one definition, an array, an async iterable, null, or undefined, directly or through a promise.
PagesForLayout intentionally uses a conservative definition-only array envelope: its array entries must be definitions, not promises.
This is a type-level restriction, not a runtime limitation: iterateGeneratedPageDefinitions consumes arrays with for await, awaiting promise entries before validation.
Resolve promised definitions before returning an array to satisfy the helper, or use an async generator to yield resolved definitions incrementally.
Nullish placeholders are invalid entries even though a nullish top-level result produces no pages.
Use a union of individually checked GeneratedPageForLayout types for heterogeneous collections; a single PagesForLayout checks one literal selected layout.
The existing explicit GeneratedPageDefinition and PagesFunction APIs remain available for dynamic or unregistered layouts.
Children follow the runtime boundary:
- Static content must match the innermost layout's accepted children type.
- Omitted,
undefined, or staticnullcontent becomes an empty string, so these forms are allowed only when that layout accepts''. - Inline renderer results are awaited and then passed through unchanged, including nullish values; a layout accepting only
nulltherefore needschildren: () => null, not staticnull. - Static callable and thenable objects are not supported by this strict helper; use an inline renderer so invocation and awaiting are explicit.
- Static objects reserve
call,apply,bind, andthenproperties to avoid accidentally accepting functions or promises through structural object compatibility. - To pass a function as a child's render value, wrap it in an inline renderer that returns the function.
See the basic example's generated guides for static and async inline content using the same registered chain.
Template files let you write any kind of file type to the dest folder while customizing the contents with global vars and explicitly subscribed global data.
Template files can be located anywhere in the src directory.
For a complete feed-generation recipe, see Generate RSS and JSON feeds.
Template files look like:
name-of-template.txt.template.ts
${name-portion}.template.tsTemplate files are .ts files that default-export one of the following sync/async functions:
Note
Wherever you see .template.ts being used, you can also use .template.js.
Type checking is supported in both file types.
See Supported file types for all available extensions.
A function that returns a string.
The name-of-template.txt portion of the template file name becomes the file name of the output file.
// name-of-template.txt.template.ts
import type { TemplateFunction } from '@domstack/static/types.js'
interface TemplateVars {
foo: string;
testVar: string;
}
const simpleTemplate: TemplateFunction<TemplateVars> = async ({
vars: {
foo,
testVar
}
}) => {
return `Hello world
This is just a file with access to global vars: ${foo}`
}
export default simpleTemplateA function that returns a single object with a content and outputName entries.
The outputName overrides the name portion of the template file name.
import type { TemplateFunction } from '@domstack/static/types.js'
interface TemplateVars {
foo: string;
}
export default async ({
vars: { foo }
}) => ({
content: `Hello world
This is just a file with access to global vars: ${foo}`,
outputName: './single-object-override.txt'
})A function that returns an array of objects with a content and outputName entries.
This template file generates more than one file from a single template file.
import type { TemplateFunction } from '@domstack/static/types.js'
interface TemplateVars {
foo: string;
testVar: string;
}
const objectArrayTemplate: TemplateFunction<TemplateVars> = async ({
vars: {
foo,
testVar
}
}) => {
return [
{
content: `Hello world
This is just a file with access to global vars: ${foo}`,
outputName: 'object-array-1.txt'
},
{
content: `Hello world again
This is just a file with access to global vars: ${testVar}`,
outputName: 'object-array-2.txt'
}
]
}
export default objectArrayTemplateAn AsyncIterator that yields objects with content and outputName entries.
import type { TemplateAsyncIterator } from '@domstack/static/types.js'
interface TemplateVars {
foo: string;
testVar: string;
}
const templateIterator: TemplateAsyncIterator<TemplateVars> = async function * ({
vars: {
foo,
testVar
}
}) {
// First item
yield {
content: `Hello world
This is just a file with access to global vars: ${foo}`,
outputName: 'yielded-1.txt'
}
// Second item
yield {
content: `Hello world again
This is just a file with access to global vars: ${testVar}`,
outputName: 'yielded-2.txt'
}
}
export default templateIteratorTemplates receive only global vars, their declared global data, and metadata for the current template.
Use global.data.ts to turn source-page collections into values a template can subscribe to.
Use the simplest return type that fits your needs:
| Return type | Multiple outputs | Custom output path | Buffers the output set | Use when |
|---|---|---|---|---|
| String | No | No (derived from template filename) | — | Single file, output path derived from template filename |
| Object | No | Yes | — | Single file with a custom output path |
| Array | Yes | Yes | Yes | Fixed set of output files known at build time |
| AsyncIterator | Yes | Yes | No | Dynamic or unknown number of outputs, or when outputs should be yielded incrementally without buffering the full set |
Start with a string return and only switch to a more complex type when you need what it provides.
All template forms can do async work (string, object, and array all support async functions).
Choose AsyncIterator specifically when the number of output files is not known until the template runs, or when you want to stream outputs one at a time rather than building the full list in memory first.