Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 210 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,198 @@ await pMap(allPosts, async (page) => {
const html = renderCache.get(page.pageInfo.path) ?? ''
```

## Generated Pages

Generated-pages files create real DomStack pages from one central module. They are similar to templates, but layout-driven: each generated definition supplies page vars and children, then DomStack renders it through the normal page and layout pipeline.

Supported filenames are:

- `*.pages.js`, `*.pages.mjs`, and `*.pages.cjs`
- `*.pages.ts`, `*.pages.mts`, and `*.pages.cts` when the current Node.js runtime supports TypeScript loading

### Generated-pages exports

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 source pages, global vars, or other discovery data |
| 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:

```ts
// src/legal.pages.ts
import type { GeneratedPageDefinition } from '@domstack/static/types.js'

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',
},
] satisfies GeneratedPageDefinition[]
```

For one static page, export a single object with the same shape instead of an array.

Use `PagesFunction` for normal functions, `async` functions, and async generators. Its type parameters are the generated page vars, the generated children type, and the default/global vars received by the factory:

```ts
// src/blog-indexes.pages.ts
import { html } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'
import type { PageData, PagesFunction } from '@domstack/static/types.js'

type SiteVars = {
siteName: string
}

type BlogIndexVars = {
layout: string
title: string
posts: PageData<Record<string, any>>[]
}

const blogIndexes: PagesFunction<BlogIndexVars, HtmlResult, SiteVars> = ({ pages, vars }) => {
const posts = pages.filter(page => page.vars.publishDate && page.pageInfo.path.startsWith('blog/'))

return {
outputName: 'blog/index.html',
vars: {
layout: 'blog-index',
title: `${vars.siteName} blog`,
posts,
},
children: ({ vars }) => html`<h1>${vars.title}</h1><p>${vars.posts.length} posts</p>`,
}
}

export default blogIndexes
```

The same type describes an async generator without requiring a separate function type:

```ts
import type { PagesFunction } from '@domstack/static/types.js'

const archivePages: PagesFunction = async function * ({ pages }) {
const years = new Set(pages.flatMap(page => {
return page.vars.publishDate ? [new Date(page.vars.publishDate).getFullYear()] : []
}))

for (const year of years) {
yield {
outputName: `blog/${year}/index.html`,
vars: { layout: 'archive', year },
}
}
}

export default archivePages
```

### Generated-pages factory parameters

Functions receive one object with:

| Parameter | Contents |
|---|---|
| `pages` | Initialized source-backed `PageData[]`. Generated pages from this or other pages files are not included. |
| `vars` | Default and global vars, before values returned by `global.data.*` are added. |
| `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. |
| `siteData` | Discovery data returned by `identifyPages()`. Its `siteData.pages` array is also source-backed only. |

Every pages file receives the same source-backed page list, so generated output does not depend on pages-file processing order. After all definitions are collected, generated pages join the full `pages` array passed to `global.data.*`, templates, page functions, and layouts.

The public `results.siteData` returned by a build remains discovery data. Generated pages are created later inside the page worker and are not added to `results.siteData.pages`.

### Generated page definitions

| Field | Behavior |
|---|---|
| `outputName` | Output path relative to the pages file's directory. It must not be absolute or contain `..` segments. Defaults to `<pages-file-name>/index.html`. |
| `vars` | Page-level vars merged with the normal default, global, layout, and builder vars. |
| `children` | Static child content or an 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 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.

### Redirect Pages

Sites migrating from another platform often need redirect pages for old URLs that no longer exist. A `*.pages.*` file can centrally generate those pages while keeping the redirect HTML in a reusable layout.

```js
// src/redirects.pages.js
// Generates one index.html per redirect entry using the redirect layout.

const redirects = [
{ from: '2020/old-slug', to: '/2020/new-slug/' },
{ from: '2021/another-old', to: '/2021/another-new/' },
]

export default function redirectsPages () {
return redirects.map(({ from, to }) => ({
outputName: `${from}/index.html`,
vars: {
layout: 'redirect',
title: 'Redirecting...',
redirectTo: to,
},
}))
}
```

```js
// src/redirect.layout.js

import { html, render } from 'fragtml'

export default function redirectLayout ({ vars }) {
return render(html`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=${vars.redirectTo}" />
<link rel="canonical" href="${vars.redirectTo}" />
<title>${vars.title}</title>
</head>
<body>
<p>Redirecting to <a href="${vars.redirectTo}">${vars.redirectTo}</a></p>
</body>
</html>`)
}
```

The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. `fragtml` escapes interpolated values by default, including attribute values and link text. Escaping does not block dangerous URL schemes like `javascript:` — keep redirect targets to known-safe URL patterns (relative paths or verified external URLs). Be careful with `from` values used in `outputName`: generated page output names must be relative and cannot contain `..` segments.

**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the object template type:

```js
// src/redirects-netlify.txt.template.js
// Generates a _redirects file for Netlify / Cloudflare Pages.

const redirects = [
{ from: '/2020/old-slug/', to: '/2020/new-slug/' },
]

export default function () {
return {
outputName: '_redirects',
content: redirects.map(({ from, to }) => `${from} ${to} 301`).join('\n'),
}
}
```

Both approaches can coexist. Copying a directory that contains a hand-crafted `_redirects` file via `--copy` is also an option when you prefer to manage redirects outside the build.

## Domstack Manifest

> [!WARNING]
Expand Down Expand Up @@ -1294,6 +1486,8 @@ export default {
}
```

The `vars` passed to a `manifestVars` function are a snapshot of page vars that can be copied from the page worker. Top-level values used only while rendering, such as functions or `PageData` objects, are left out of this snapshot.

Only values selected by `manifestVars` are copied into public manifest entries. Root `policy` is emitted once on the manifest. This avoids leaking arbitrary page vars while still letting service workers, Workbox hooks, and deployment tools consume a stable manifest-level policy shape.

### Manifest built hooks
Expand Down Expand Up @@ -1613,7 +1807,7 @@ Use this to filter the domstack manifest before hooks receive it, before domstac

```js
/**
* @import { DomstackManifestEntry } from '@domstack/static'
* @import { DomstackManifestEntry } from '@domstack/static/types.js'
*/

export default {
Expand Down Expand Up @@ -1872,22 +2066,26 @@ import type {
AsyncPageFunction,
TemplateFunction,
TemplateAsyncIterator,
PagesFunction,
// Data/param types
PageData,
PageInfo,
TemplateInfo,
PagesFileInfo,
GeneratedPageDefinition,
LayoutFunctionParams,
GlobalDataFunctionParams,
PageFunctionParams,
TemplateFunctionParams,
PagesFunctionParams,
} from '@domstack/static/types.js'
```

> **Note:** All function types have both synchronous and asynchronous variants (e.g., `LayoutFunction` and `AsyncLayoutFunction`). Use the async variants when your function is an `async` function.
> **Note:** Page, layout, and global-data functions have synchronous and asynchronous variants. `PagesFunction` covers normal functions, `async` functions, and async generators because generated-pages factories can return definitions, promises, or async iterables.

They are all generic and accept a variable template that you can develop and share between files.
The function types are generic and accept variable shapes that you can develop and share between files.

The data and param types (`PageData`, `PageInfo`, `TemplateInfo`, `*FunctionParams`) are useful when you want to annotate variables or helper functions that receive these objects without using the function types directly:
The data and parameter types (`PageData`, `PageInfo`, `TemplateInfo`, `PagesFileInfo`, `GeneratedPageDefinition`, and `*FunctionParams`) are useful when you want to annotate variables or helper functions that receive these objects without using the function types directly:

```ts
import type { GlobalDataFunctionParams, PageData, PageInfo } from '@domstack/static/types.js'
Expand All @@ -1900,9 +2098,9 @@ function getPublishedPages({ pages }: GlobalDataFunctionParams): PageData[] {
}
```

#### Advanced Type Parameters for PageFunction and LayoutFunction
#### Advanced type parameters

`PageFunction` and `LayoutFunction` support additional template parameters for precise return type control:
`PageFunction`, `LayoutFunction`, and `PagesFunction` support additional type parameters for precise input and return type control:

**PageFunction<T, U>**
- `T` - The type of variables passed to the page (required)
Expand All @@ -1913,7 +2111,12 @@ function getPublishedPages({ pages }: GlobalDataFunctionParams): PageData[] {
- `U` - The type of content received from pages as `children` (optional, defaults to `any`)
- `V` - The return type of the layout function (optional, defaults to `string`)

This allows pages to return custom types (like VDOM or JSON) while ensuring layouts produce HTML strings:
**PagesFunction<T, U, V>**
- `T` - The vars added to generated pages (optional, defaults to `Record<string, any>`)
- `U` - The static children or inline page-function return type (optional, defaults to `any`)
- `V` - The default and global vars received by the pages factory (optional, defaults to `Record<string, any>`)

This allows pages to return custom types (like VDOM or JSON), ensures layouts produce HTML strings, and keeps generated-page vars separate from the vars used to create them:

```ts
// Define custom types
Expand Down
Loading