Generated pages
Some pages don't have a source file of their own: one page per tag, one archive per month. A page template generates many pages from one file.
Bracketed names
Write a path segment as [name] to make it a parameter. The source is a template
if its file name or any folder above it is a parameter:
| Source | URL pattern | Example URLs |
|---|---|---|
tags/[tag].jsx |
/tags/[tag] |
/tags/go, /tags/plan9 |
logs/[year]/index.jsx |
/logs/[year] |
/logs/2025, /logs/2026 |
logs/[year]/[month].jsx |
/logs/[year]/[month] |
/logs/2026/11 |
tags/[tag]/feed.xml.jsx |
/tags/[tag]/feed.xml |
/tags/go/feed.xml |
Names are lowercase letters and digits, starting with a letter, and each name
appears at most once in a path. The whole segment must be the parameter: [year]-archive.jsx is an error, not a page.
Templates must be JSX. A Markdown or Go template file inside a bracketed path is
an error, and so is a static file or _-prefixed file inside a bracketed folder.
Put layouts, defaults, and output templates for generated pages in a real folder
above the bracketed one (see
Layouts and data
).
Declaring the pages with params()
A template starts with a params(...) call that lists the parameter values to
generate. It takes an array of objects, or a function returning one, with
exactly the template's parameter names as keys:
// tags/[tag].jsx
params(() => [...new Set(pages("**").flatMap(p => p.tags ?? []))].map(tag => ({ tag })));
data({ title: `Tagged ${page.params.tag}` });
<ul>
{pages({ where: { tags: page.params.tag } }).reverse().map(p =>
<li><a href={p.path}>{p.title}</a></li>
)}
</ul>
// logs/[year]/[month].jsx
params(() => pages("logs/**").map(l => ({
year: l.date.slice(0, 4),
month: l.date.slice(5, 7),
})));
data({ title: `${page.params.year}-${page.params.month}` });
<ul>
{pages("logs/**")
.filter(l => !l.params && l.date.startsWith(`${page.params.year}-${page.params.month}`))
.map(l => <li><a href={l.path}>{l.title ?? l.date}</a></li>)}
</ul>
Rules:
-
params()must be among the template's leading statements, before the markup, alongside anydata()call. Anywhere else it's an error. -
Values are strings or numbers (numbers become strings). Each value must be a
non-empty single path segment: no
/or\, not.or.., and not starting with.or_. - Duplicate sets are merged, so you don't need to deduplicate. An empty array generates no pages.
- A set with a missing or extra key, or a bad value, fails the build, naming the template and the set.
What params() can see
params() runs before any generated page exists, in its own VM with your
globals and pages(). page is undefined there. pages() returns only real
pages, never generated ones, so one template can't feed another and the set of
pages depends only on the files on disk.
Once all pages exist, page bodies and layouts see everything: a "Tags" index
page can call pages("tags") and list every generated tag page.
Generated page data
A generated page's data is built like any page's, plus params:
| Key | Value for /logs/2026/11 |
|---|---|
params |
{ year: "2026", month: "11" } (always strings) |
path |
/logs/2026/11 |
slug |
11 |
src |
The template's source |
isDir |
True if the template is an index.jsx |
Layouts and data for generated pages
Globals, defaults, layouts, and output templates are found by the generated
page's path, exactly as if a real file lived there. logs/_layout.jsx wraps /logs/2026/11, and a real logs/2026/_defaults.jsx applies to it. HTML
generated pages get outputs from _outputs/ like any page, so /logs/2026/11.txt exists if _outputs/txt.jsx does.
If a generated page's parent folder doesn't exist as a page, taragen adds a
grouping node for it, so pages("logs/*") reaches years that exist only through
generated pages.
Overriding a generated page
A real source at the same path wins, silently. To give one tag a hand-written
page with its own intro, add tags/plan9.md; every other tag keeps its generated
page. A generated page at the path of a grouping node fills it, keeping its
subpages: that's how logs/[year]/index.jsx gives /logs/2026 an index.
Two templates producing the same path is an error, as is a generated page whose URL matches a static file, output, alias, or redirect.
Generated pages in listings
Generated pages show up in pages() results, including ** patterns. A home
stream built from pages("logs/**") will include generated month pages; skip
them with a filter on params:
pages({ path: ["articles/**", "logs/**"] }).filter(p => !p.params).reverse()
Drafts and serving
params() follows the draft rules: in build its pages() calls never see
drafts, so a month that only holds a draft gets no page; in serve it does. A
generated page whose own data makes it a draft is skipped by build.
In serve, a URL that fits the pattern but wasn't generated, like /tags/nosuchtag, isn't a page. Adding a tag to a Markdown file creates its page
on the next reload.