# Reference

---

# Directory and file names

_Trail / Reference_

> The complete naming grammar — order prefixes, slugs, type suffixes, appendix orders — plus exactly what each kind of directory may contain, the reserved names, and the sorting and duplicate rules.

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](/trail/getting-started/anatomy-of-a-site.md) 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](/trail/structuring-a-site/link-aliases.md). |
| `<slug>.<ext>` | `logon-flow.svg` | A co-located [image](/trail/writing/images.md). 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](/trail/structuring-a-site/books.md)**, 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`, no `first`.

### `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.
- `a0` is an error: appendix orders start at `a1`.
- Outside a book, an `a<N>--` prefix is just a non-numeric order prefix, and therefore an error.

### `<slug>`

Lowercase kebab-case:

```text
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`](/trail/writing/links-and-references.md).

### `<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](/trail/reference/trail-toml.md) |
| 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:

```text
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:

```text
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.

```text
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_

> Every key of every trail.toml — the site root, products, anthologies, books, topics, subfolders, chapters and shelves — with types, defaults, validation rules and what each one reaches.

`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. |

```toml
sitename = "Peios Learn"
title = "Documentation for everything Peios"
description = "Concepts, guides, and reference for the Peios operating system…"
url = "https://learn.peios.org"
accent = "#3b82f6"
featured = ["peios", "pekit", "provium", "universal-directory", "trail"]
footer = "Peios Learn — documentation for the Peios project."

[[nav]]
label = "Specs"
url = "~peios/pgss"
```

### Validation

- `accent` and `accent_dark` must be `#rrggbb`. `accent_dark` without `accent` is an error.
- `custom_css`, `favicon` and `head_html` are 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_url` must contain the literal `{path}`, which is replaced with the article's source path relative to the site root.
- Every slug in `featured` must name an existing product.

### `passthrough`

Root entries trail neither builds nor understands, copied into the output as they are:

```toml
passthrough = ["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](/trail/writing/links-and-references.md). |

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.

> [!IMPORTANT]
> `[[nav]]` blocks must come **last in the file**. TOML gives every key after a table header to that table, so a `footer =` written below them is read as a nav item's field and fails with `unknown field 'footer', expected 'label' or 'url'`. Put every plain key above the first `[[nav]]`.

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. |

```toml
title = "pekit"
monogram = "pk"
color = "#22c55e"
description = "The recipe-driven build and packaging tool."
inline_ref = ["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. |

```toml
title = "Peios Generic System Standards"
short = "PGSS"
description = "Cross-platform system standards from the Peios Project"
inline_ref = ["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. |

```toml
title = "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](/trail/writing/inline-references.md).

---

# Frontmatter reference

_Trail / Reference_

> Every key an article's YAML frontmatter may carry, its type, whether it is required, what it feeds, and how it is validated — with the differences between topic articles and book articles set out.

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.**

```markdown
---
title: Command-line reference
type: reference
description: Every pekit flag with its value form, the capability matrix, and exit codes.
related:
  - pekit/running/invocation
  - pekit/reference/recipe-format
inline_ref:
  - pekit CLI
updated: 2026-05-01
reading_minutes: 25
---
```

## 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">` and `og:description`;
- the standfirst line of the page's [markdown mirror](/trail/building/the-output-surface.md);
- the `description` field in `site.json`.

It is not shown on the page, and it is not the in-site search snippet — Pagefind builds those from body text.

[`--strict`](/trail/reference/cli.md) fails the build on any article without one. [Alias pages](/trail/structuring-a-site/link-aliases.md) are exempt.

### `related`

Page references in the [`~` grammar](/trail/writing/links-and-references.md), **without the leading `~`**:

```yaml
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:

```yaml
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](/trail/writing/inline-references.md).

### `updated`

An ISO `YYYY-MM-DD` date, validated as such:

```text
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.

> [!TIP]
> Both components must be zero-padded to two digits: `2026-05-01`, never `2026-5-1`. No quoting is needed — an unquoted date works.

### `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](/trail/structuring-a-site/link-aliases.md) 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_

> Every trail command and flag, with defaults, exit status, what goes to stdout and stderr, and the complete list of build-time errors and warnings.

Trail has two commands, `build` and `serve`, plus clap's own `help`. Everything below is the complete flag surface; there are no others.

## Synopsis

```text
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](/trail/building/the-cache-busting-copy.md) of the whole site under `PATH`, with every link inside it rewritten to stay there. |

On success:

```text
built 1227 pages (8 products) → ./dist
```

With `--cbpath`, a second line names the copy:

```text
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`. |

```text
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 build` output never contains it.**
- Misses serve the built `404.html` with 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:

```text
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:

```text
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:

```console
$ 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.
