Reference
Single-page view · as markdown
Directory and file names
Trail / Reference
Trail derives the whole site from the tree, so every name in it is meaningful and every unrecognised name is a build error. This page is the exhaustive grammar. Anatomy of a site is the same material as a walkthrough.
The name shapes #
| Shape | Example | What |
|---|---|---|
<slug>.product | pekit.product | A product. Site root only; no order prefix. |
<order>--<slug>.antho | 1--security.antho | An anthology. |
<order>--<slug>.topic | 4--reference.topic | A topic. |
<order>--<slug>.book | 2--pgss.book | A book. |
<order>--<slug>.shelf | 3--tooling.shelf | A shelf. |
<order>--<slug> | 300--tokens | A topic subfolder or a book chapter. No suffix. |
<order>--<slug>.md | 100--sids.md | An article. |
<order>--<slug>.link | 3--faq.link | A link alias. |
<slug>.<ext> | logon-flow.svg | A co-located image. No order prefix. |
trail.toml | Configuration, at every level. |
<order> #
A non-negative integer, parsed as written. 1, 100 and 007 are all valid and all mean their numeric value.
- It is a sort key only — never shown, never part of a URL — except inside a book, where it is the published section number.
- Gaps are fine and normal.
- Two siblings sharing an order is an error, since their relative order would then depend on nothing visible.
- Anything not parseable as an integer is an error: no
1.5, no-1, nofirst.
a<N> — appendix orders #
Inside a book only. An order written a1, a2, … marks an appendix entry, which is lettered rather than numbered and sorts after every numbered sibling at its level.
- Letters are bijective base 26:
a1→ A,a26→ Z,a27→ AA. - The letter comes from the number, not from position, so gaps show through here too.
a0is an error: appendix orders start ata1.- Outside a book, an
a<N>--prefix is just a non-numeric order prefix, and therefore an error.
<slug> #
Lowercase kebab-case:
ASCII lowercase letters, digits and hyphens
not empty
not starting or ending with a hyphen
getting-started, pgss, rfc-2119 are fine. Getting_Started, -draft, tokens/, café are not.
The slug is the URL segment, and it is what you write in a ~link.
<kind> #
Exactly one of antho, topic, book, shelf. Any other suffix is an error, as is a grouping directory with no suffix at all in a place where one is expected.
What each directory may contain #
Anything not listed is a build error. Entries whose name starts with . are skipped everywhere, so .git, .DS_Store and editor droppings are invisible to trail.
| Directory | Subdirectories | Files |
|---|---|---|
| Site root | *.product, the output dir | trail.toml; whatever favicon, custom_css, head_html and passthrough name |
| Product | .antho, .topic, .book, .shelf | trail.toml |
| Anthology | .antho, .topic, .book, .shelf | trail.toml |
| Shelf | .antho, .topic, .book — never another .shelf | trail.toml |
| Topic | <order>--<slug> subfolders | trail.toml, .md, .link, images |
| Topic subfolder | (none — one level only) | trail.toml, .md, .link, images |
| Book | <order>--<slug> chapters | trail.toml, .md, .link, images |
| Book chapter | <order>--<slug> chapters, to any depth | trail.toml, .md, .link, images |
The dividing line: directories that hold groupings hold no files but trail.toml; directories that hold articles hold no grouping directories.
trail.toml by level #
| Level | Required? | Keys |
|---|---|---|
| Site root | required | see the trail.toml reference |
| Product | required | title, monogram, color, description, inline_ref |
| Anthology | required | title, description, inline_ref |
| Book | required | title, description, short, inline_ref |
| Topic | optional | title, inline_ref |
| Topic subfolder | optional | title, inline_ref |
| Book chapter | optional | title, inline_ref |
| Shelf | optional | title only — inline_ref is an error |
Unknown keys are errors at every level. Where title is optional and absent, it is derived from the slug: hyphens to spaces, each word capitalised (the-two-gates → "The Two Gates").
Reserved names #
| Name | Where | Why |
|---|---|---|
print | article slugs | every container publishes a /print page |
assets | product slugs | collides with /assets in the output |
pagefind | product slugs | collides with /pagefind in the output |
Images #
A co-located image is <slug>.<ext> where the stem is a valid slug and the extension is lowercase and one of:
png jpg jpeg gif svg webp avif
Anything else in an article directory falls through to the "unexpected file" error.
Sorting #
Siblings sort by order, ascending. Inside a book, appendix entries sort after every numbered entry at the same level, whatever their numbers.
Products are the exception: no order prefix, so they sort by the root config's featured list first (in that list's order), then the rest by title.
Duplicates #
Within one container, both order and slug must be unique:
items 'concepts' and 'guides' share order 1
duplicate article slug 'overview'
The noun is item for a container's mixed children (topics, books, anthologies, shelves), and article inside a topic subfolder or where only articles can sit. Slugs are reported without their order prefixes.
Shelf contents are checked flattened into their parent, because a shelf adds no URL segment — two shelves in one product cannot both hold a getting-started topic, since both would want the same URL.
URLs #
A page's URL is the product slug, then the slug of every container between it and the page, with two exceptions: shelves contribute nothing, and order prefixes contribute nothing.
peios.product/1--security.antho/2--tokens.topic/300--issuing/100--flow.md
→ /peios/security/tokens/issuing/flow
Pages are written to disk as <path>/index.html.
trail.toml reference
Trail / Reference
trail.toml appears at every level of the tree, with a different schema at each. Unknown keys are build errors everywhere, so a typo fails loudly instead of doing nothing.
The site root #
Required at the site root. Four keys are mandatory.
| Key | Type | Default | |
|---|---|---|---|
sitename | string | required | The site's identity: header wordmark (last word accented), <title> suffix, search placeholder, og:site_name. |
title | string | required | The front page's headline; its og:title and the base for its meta description. |
description | string | required | The front page's standfirst and meta description; the first line of llms.txt. |
footer | string | required | The line at the foot of every page. |
url | string | none | The site's public base URL, e.g. https://learn.peios.org. Trailing slashes are trimmed. |
featured | array of strings | [] | Product slugs, in order. Sorts products, and selects which appear on the front page. |
accent | #rrggbb | theme default | The site accent colour. |
accent_dark | #rrggbb | derived | The dark-mode accent. Derived by mixing accent 75% toward white when absent. |
custom_css | path | none | A stylesheet shipped at /assets/custom.css, loaded after the built-in one. |
favicon | path | none | Shipped at the output root under its own name, linked from every page. |
head_html | path | none | A file injected verbatim at the end of every page's <head>. |
edit_url | string | none | "Edit this page" URL template. Must contain {path}. |
nav | array of tables | [] | Header links. See below. |
passthrough | array of paths | [] | Root entries copied into the output verbatim. See below. |
product_theming | bool | true | Whether each product tints its own pages. |
built_by_trail | bool | true | Whether "Built with Trail." appears under the footer. |
= "Peios Learn"
= "Documentation for everything Peios"
= "Concepts, guides, and reference for the Peios operating system…"
= "https://learn.peios.org"
= "#3b82f6"
= ["peios", "pekit", "provium", "universal-directory", "trail"]
= "Peios Learn — documentation for the Peios project."
[[]]
= "Specs"
= "~peios/pgss"
Validation #
accentandaccent_darkmust be#rrggbb.accent_darkwithoutaccentis an error.custom_css,faviconandhead_htmlare paths relative to the site root, and the file must exist. Naming one is also what makes it legal to keep in the site root; the first path component is what is tolerated there.edit_urlmust contain the literal{path}, which is replaced with the article's source path relative to the site root.- Every slug in
featuredmust name an existing product.
passthrough #
Root entries trail neither builds nor understands, copied into the output as they are:
= ["CNAME", ".well-known", "ads.txt"]
Each entry is a plain relative path naming a file or a directory that exists in the site root; directories are copied whole, nesting included. Naming an entry is also what makes it legal to keep in the site root, exactly as favicon and friends do — so this is how a repository that is also a trail site keeps its CNAME beside its trail.toml.
Errors: an entry that is empty, absolute, or contains ..; one naming something that does not exist; one naming the build output directory.
Passthrough entries are copied last, after everything trail generates, so an entry deliberately named after a generated file replaces it. Naming robots.txt is a supported way to ship your own; naming assets would bury the stylesheet, and trail will not stop you.
[[nav]] #
| Key | Type | |
|---|---|---|
label | string | required. The link text. |
url | string | required. An external URL, a root-relative path, or a ~ page reference. |
References here are resolved strictly at load time — a broken nav link fails the build even under --allow-dangling-links, because site chrome appears on every page.
A shelf is not a link target, so a nav entry cannot point at one — aim at the anthology above it, or a page inside it.
What url unlocks #
Without it: no sitemap.xml, no Sitemap: line in robots.txt, no og:url, relative canonical URLs, and relative outward links in /print bundles. Set it before deploying.
A product #
Required in every <slug>.product directory.
| Key | Type | |
|---|---|---|
title | string | required. The product's display name. |
monogram | string | required. The short string on the product's tile. |
color | #rrggbb | required. The product accent. |
description | string | required. One sentence: the card text, the page standfirst, the llms.txt entry. |
inline_ref | array of strings | Phrases linking to the product's landing page. |
= "pekit"
= "pk"
= "#22c55e"
= "The recipe-driven build and packaging tool."
= ["pekit"]
An anthology #
Required in every .antho directory.
| Key | Type | |
|---|---|---|
title | string | required. |
description | string | required. The card text, the page standfirst, the llms.txt entry. |
inline_ref | array of strings | Phrases linking to the anthology's landing page. |
A book #
Required in every .book directory.
| Key | Type | |
|---|---|---|
title | string | required. |
description | string | required. |
short | string | An abbreviation used where the full title will not fit: the sidebar heading, the card pill, the line above the cover title. |
inline_ref | array of strings | Phrases linking to the book's cover. These may carry a § section suffix in prose. |
= "Peios Generic System Standards"
= "PGSS"
= "Cross-platform system standards from the Peios Project"
= ["PGSS", "Peios Generic System Standards"]
A topic, subfolder or chapter #
Optional. Both keys are optional.
| Key | Type | |
|---|---|---|
title | string | Overrides the slug-derived title. |
inline_ref | array of strings | Phrases linking to the container's first article. Declaring them on a container with no articles is an error. |
= "Reference"
A shelf #
Optional, and takes title only.
| Key | Type | |
|---|---|---|
title | string | Overrides the slug-derived title. |
inline_ref on a shelf is an error. A shelf has no page and no articles of its own, so the phrase would have nowhere to land — declare it on the shelf's items instead.
Derived titles #
Wherever title is optional and absent, it comes from the slug: hyphens become spaces and each word is capitalised. 2--writing-tests.topic becomes "Writing tests"; the-two-gates becomes "The Two Gates". Write the key when that is wrong — acronyms, proper nouns, unusual casing.
inline_ref everywhere #
The key means the same thing at every level: a list of phrases that should become links to this thing wherever they appear in the site's prose. Phrases are exclusive site-wide — two declarers claiming one phrase is an error — and must not be empty, carry leading or trailing whitespace, or contain §. See Inline references.
Frontmatter reference
Trail / Reference
Every article begins with a YAML frontmatter block: a line of exactly ---, the keys, and a closing ---. It must be the first thing in the file. Unknown keys are build errors.
- - -The keys #
| Key | Type | Topic article | Book article |
|---|---|---|---|
title | string | required | required |
type | string | required | error |
description | string | optional | optional |
related | array of strings | optional | optional |
inline_ref | array of strings | optional | optional |
updated | ISO date | optional | optional |
reading_minutes | integer | optional | optional |
title #
The page's name: its h1, breadcrumb, sidebar entry, search-result heading and og:title. Never derived — an article always states its own.
type #
The learn taxonomy. Required on articles in topics; an error on articles in books, where the question it answers is already settled by the document's order.
Trail does not constrain the value and does not display it on the page. It is carried into site.json as the article's type. This site uses concept, how-to and reference.
description #
One sentence. It feeds:
<meta name="description">andog:description;- the standfirst line of the page's markdown mirror;
- the
descriptionfield insite.json.
It is not shown on the page, and it is not the in-site search snippet — Pagefind builds those from body text.
--strict fails the build on any article without one. Alias pages are exempt.
related #
Page references in the ~ grammar, without the leading ~:
related:
- pekit/running/invocation
- peios/package-management/overview
They render as a "Related Content" list under the article, appear in the markdown mirror, and become a list of resolved paths in site.json.
Resolution is as strict as a body link: ambiguous references always fail the build; missing ones fail unless --allow-dangling-links downgrades them to a warning, in which case they are dropped from the list.
inline_ref #
Phrases that should link to this article wherever they appear in the site's prose:
inline_ref:
- PCDS GUID
- PCDS GUIDs
Matching is literal, whole-word and longest-first, and skips headings, links, image alt text, code, and the claiming page itself. A phrase belongs to exactly one declarer site-wide; a collision is a build error, as is an empty phrase, one with leading or trailing whitespace, or one containing §. Alias pages claim nothing. See Inline references.
updated #
An ISO YYYY-MM-DD date, validated as such:
updated: '2026/05/01' is not an ISO date (expected YYYY-MM-DD)
It appears in the page's meta line and becomes the page's <lastmod> in sitemap.xml. Containers take the most recent date beneath them.
Deliberately manual: file modification times are noise, and trail does not read git history.
reading_minutes #
An integer overriding the estimate, which is otherwise the body's word count at 200 words a minute, rounded up, with a floor of 1. Code blocks count toward the word count.
Containers sum their children's values, so a book cover reports the whole book.
Errors #
| Message | Cause |
|---|---|
… has no frontmatter (must start with '---') | The file does not open with ---. |
… has unterminated frontmatter | No closing ---. |
parsing frontmatter of … | Invalid YAML, a missing required key, or an unknown key. |
updated: '…' is not an ISO date (expected YYYY-MM-DD) | A malformed date. |
Inside a book, type: produces an unknown-key error, since type is not part of the book article schema.
Frontmatter and .link aliases #
An alias page has no frontmatter of its own — it renders the target's body. It takes its title from the .link file (or from its own slug), its canonical URL from the original, and it claims no inline_ref phrases. Inside a book it also loses type and description, as every book entry does.
Command-line reference
Trail / Reference
Trail has two commands, build and serve, plus clap's own help. Everything below is the complete flag surface; there are no others.
Synopsis #
trail build [OPTIONS] [ROOT]
trail serve [OPTIONS] [ROOT]
trail help [COMMAND]
trail --version prints the version; trail --help and trail <command> --help print usage.
trail build #
Builds the site into the output directory.
| Argument | ||
|---|---|---|
[ROOT] | path | The site root — the directory holding trail.toml. Default . |
| Flag | Value | |
|---|---|---|
--out <DIR> | path | Where the site is written. Default ROOT/dist. Excluded from the site scan, so a build into a directory inside the root is safe. |
--allow-dangling-links | — | Downgrade missing ~link and related: targets, missing images, and unresolvable § citations from errors to warnings. Ambiguous references stay fatal. |
--strict | — | Fail the build when any article has no description:. Alias pages are exempt. |
--render-llms-full | — | Also write each unit's print.md as llms-full.txt beside it. |
--cbpath <PATH> | one path segment | Also publish a cache-busting copy of the whole site under PATH, with every link inside it rewritten to stay there. |
On success:
built 1227 pages (8 products) → ./dist
With --cbpath, a second line names the copy:
built 1227 pages (8 products) → ./dist
cache-busting copy → ./dist/8f3a91c/
The page count is the site's; the copy holds the same number again and is not added in.
The count is HTML pages: articles, product and anthology landings, book covers, /print pages, the front page and 404.html. Markdown mirrors, llms.txt, site.json, the sitemap and the search bundle are not counted.
trail serve #
Builds the site, serves the output over HTTP, and rebuilds on change. Takes every build flag, plus:
| Flag | Value | |
|---|---|---|
--addr <ADDR> | socket address | Default 127.0.0.1:8724. |
serving ./dist at http://127.0.0.1:8724 (watching . for changes)
rebuilt 1227 pages
Behaviour:
- Changes are debounced for 200 ms; changes inside the output directory, and paths with a dot-prefixed component, are ignored.
- Pages are served with a live-reload script that reloads them over server-sent events after each successful rebuild.
trail buildoutput never contains it. - Misses serve the built
404.htmlwith a real 404 status. - A failed initial build or rebuild prints the error and keeps serving the last good output — the process does not exit.
Exit status and streams #
Exit 0 | The build succeeded. |
| Exit non-zero | The build failed; the error is on stderr. |
| stdout | The build summary line, and serve's serving/rebuild lines. |
| stderr | Errors and warnings. |
trail serve runs until interrupted.
Warnings #
These print on stderr and do not fail the build:
| Warning | |
|---|---|
image '…' is not referenced by any article and was not published | The file stays out of the output. |
in article '…': link '~…' matches no page in product '…' | Only with --allow-dangling-links. |
in article '…' (related): … | A related: reference that did not resolve, with --allow-dangling-links. |
in article '…': image '…' not found … | With --allow-dangling-links. |
initial build failed (serving previous output): … | serve only. |
rebuild failed (still serving the last good build): … | serve only. |
Errors #
Loading errors stop at the first problem, with the chain of contexts:
Error: loading product 'pekit'
Caused by:
0: loading topic in '2--recipes.topic'
1: in article '100--anatomy.md'
2: updated: '2026-5-1' is not an ISO date (expected YYYY-MM-DD)
Link errors are collected across the whole site and reported together:
Error: 3 broken links:
in article '/pekit/recipes/anatomy': link '~pekit/running/invokation' matches no page in product 'pekit'
…
The common ones #
| Message | |
|---|---|
unexpected file '…' in site root: only trail.toml is allowed | The root takes trail.toml, *.product/, the output dir, and files named by the config. |
unexpected directory '…' in site root: only *.product directories are allowed | |
unexpected grouping type '.…' on '…' in a product (expected .antho, .book, .topic or .shelf) | |
a shelf cannot contain another shelf ('…') | |
grouping directory '…' is missing its '<order>--' prefix | |
'…' has a non-numeric order prefix '…' | |
'…' is not a valid slug (lowercase kebab-case) | |
items '…' and '…' share order N | Two siblings with the same order prefix. |
duplicate article slug '…' | Also raised for shelf contents flattened into their parent. |
chapter '…' contains no articles | A book chapter must lead somewhere. |
'print' is a reserved slug (every section has a /print page) | |
'…' is a reserved product slug (it collides with the '/…' output directory) | assets and pagefind. |
… has no frontmatter (must start with '---') | |
… has unterminated frontmatter | |
parsing frontmatter of … | Bad YAML, a missing required key, or an unknown key. |
updated: '…' is not an ISO date (expected YYYY-MM-DD) | |
color '…' is not a #rrggbb hex color | |
accent_dark without accent: set the base accent color too | |
edit_url must contain a {path} placeholder … | |
featured product '…' does not exist (no ….product directory) | |
favicon names '…', which does not exist in the site root | Likewise custom_css and head_html. |
link '~…' is ambiguous; candidates: … | Never downgradable. |
unknown admonition type '[!…]' (expected NOTE, TIP, IMPORTANT, WARNING or CAUTION) | |
unclosed ':::tabs' block (close it with ':::') | And the other tab-group errors. |
inline_ref phrase '…' is claimed by both … and … | |
'… §…' does not match any section of '…' | Downgradable. |
image '…': ~references name pages, not files — use a path relative to the article | |
--cbpath '…' is already a path in the site; the copy would bury it … | Checked before anything is built. |
--cbpath '…' must be a single path segment, with no '/' | Likewise the empty, dot-leading and bad-character forms. |
Installing #
Trail is a single Rust binary. Build it from its source directory:
$ cargo build --release
The result is target/release/trail. It carries the templates, stylesheet, fonts, syntax definitions, search engine and diagram renderer inside it, so the binary alone is the whole tool — copy it onto your PATH and nothing else needs installing.