Building
Single-page view · as markdown
Building and serving
Trail / Building
Trail has two commands. build writes the site; serve writes it and then serves it, rebuilding as you edit.
trail build #
$ trail build [ROOT] [--out DIR] [--strict] [--allow-dangling-links] [--render-llms-full] [--cbpath PATH]
ROOT is the directory containing trail.toml, defaulting to the current directory. Output goes to ROOT/dist unless --out says otherwise.
$ trail build
built 1227 pages (8 products) → ./dist
The page count is HTML pages: articles, product and anthology landings, book covers, /print bundles, the front page and the 404 page. The markdown mirrors, llms.txt, site.json, the sitemap and the search index are not counted as pages.
The output directory belongs to trail #
At the end of every successful build, trail deletes everything in the output directory it did not write on that run, then removes any directory left empty. Pages for articles you deleted, superseded search-index fragments, images you stopped referencing: all gone.
Two consequences:
- Do not keep anything in
dist/by hand. ACNAME, a_headersfile, a stray favicon — trail will delete it. Anything that must ship belongs in the source tree and named bypassthrough, which copies it in on every build. - A failed build prunes nothing. The previous output is left completely intact, which is what makes the dev server able to keep serving a working site while you fix an error.
Files whose contents have not changed are left untouched rather than rewritten, so modification times stay stable for rsync-style deploys.
When a build fails #
Loading errors — a bad name, an unknown config key, a malformed frontmatter block — stop the build at the first problem, with the context chained on:
Error: loading product 'pekit'
Caused by:
0: loading topic in '2--recipes.topic'
1: loading article 'anatomy'
2: ./pekit.product/2--recipes.topic/100--anatomy.md has unterminated frontmatter
Link errors are different: they are collected across the whole site and reported together, because fixing them one build at a time would be miserable.
Error: 3 broken links:
in article '/pekit/recipes/anatomy': link '~pekit/running/invokation' matches no page in product 'pekit'
…
Some problems are warnings on stderr rather than errors, and do not stop the build: an image nothing references, and (with --allow-dangling-links) missing link targets.
The flags #
--allow-dangling-links #
Downgrades missing ~link and related: targets from errors to warnings, and does the same for missing images and unresolvable § citations. Ambiguous references stay fatal — trail will not pick between two candidate pages.
It is for the middle of a large reorganisation, when you want to see the site before every reference has caught up. Shipping with it on means shipping dead links.
--strict #
Fails the build when any article has no description::
Error: --strict: 2 articles without a description:
article '/pekit/recipes/sources' has no description
article '/pekit/reference/cli' has no description
Descriptions become meta descriptions and social-preview text, so a page without one is a page that presents badly everywhere outside the site. Alias pages are exempt — the original carries the description.
Use it in CI; leave it off while drafting.
--render-llms-full #
Writes an extra copy of each unit's print.md as llms-full.txt beside it. Purely a discovery fallback for agents that probe for that filename instead of reading llms.txt, which always points at print.md anyway. Off by default, because it doubles a large part of the output for no new information.
--cbpath PATH #
Publishes a second copy of the whole site under PATH, with every link inside it rewritten to stay inside it. Name it after something that changes on every deploy — a commit hash — and you have a URL nothing can have cached, so what comes back is necessarily this build. See The cache-busting copy.
--out DIR #
Writes the site somewhere other than ROOT/dist. The chosen directory is excluded from the site scan, so a build into a directory inside the site root does not turn the previous build into content.
trail serve #
$ trail serve [ROOT] [--addr ADDR] [build flags]
serve takes every build flag, plus --addr (default 127.0.0.1:8724).
$ trail serve
serving ./dist at http://127.0.0.1:8724 (watching . for changes)
It builds the site, serves the output directory over HTTP, and watches the source tree. On a change it debounces for 200 ms of quiet — editors write in bursts — rebuilds, and tells every open page to reload itself over a server-sent-events stream. Changes inside the output directory are ignored, so the build cannot trigger itself, as are paths with a dot-prefixed component, so a .git write does not either.
rebuilt 1227 pages
A broken site does not kill the server. A failed initial build or rebuild prints the error and keeps serving the last good output:
rebuild failed (still serving the last good build): loading product 'pekit': …
Fix the file and the next successful rebuild reloads the page.
Two details worth knowing:
- The live-reload script is only ever injected by
serve. Output written bytrail buildnever contains it. - Misses serve the built
404.htmlwith a real 404 status, exactly as a static host configured for that file would — so you can check the not-found page locally.
Deploying #
The output directory is plain static files. Any host that serves a directory will do.
Two things to configure on the host:
404.htmlas the not-found page. Trail writes it; the host has to be told to use it.- Directory indexes. Pages are written as
<path>/index.html, so the URL/pekit/reference/clineeds to serve/pekit/reference/cli/index.html. Nearly every static host does this by default.
Set url in the root trail.toml before deploying: without it there is no sitemap.xml, no Sitemap: line in robots.txt, and canonical and og:url tags stay relative. See Configuring the site.
If the host's caching is aggressive enough that you cannot tell a fresh deploy from a stale one — and on GitHub Pages you cannot, because the headers are not yours to set — build with --cbpath "${GITHUB_SHA::8}" and you get a URL per deploy that nothing has ever cached. See The cache-busting copy.
Configuring the site
Trail / Building
One trail.toml at the site root configures the whole site. It is the only place these settings live — there is no per-environment config, no CLI equivalent for most of them, and unknown keys are build errors rather than typos you find out about later.
= "Peios Learn"
= "Documentation for everything Peios"
= "https://learn.peios.org"
= "#3b82f6"
= "Concepts, guides, and reference for the Peios operating system and every project around it — from the kernel to the toolchain that builds it."
= ["peios", "pekit", "provium", "universal-directory", "trail"]
= "Peios Learn — documentation for the Peios project."
The trail.toml reference is the exhaustive table; this page is what each setting is for.
The four required keys #
sitename, title, description and footer must all be present.
sitename is the site's identity: the wordmark in the header, the suffix on every page's <title>, the search box's placeholder, and og:site_name. Its last word is accented in the wordmark — "Peios Learn" — so a two-word name reads best. A one-word name simply gets no accent.
title and description are the front page's headline and standfirst, and its meta description and social-preview text. The description is also the first line of llms.txt, so write it as a summary of the whole site rather than a slogan.
footer is the line at the foot of every page. Beneath it, "Built with Trail." appears unless built_by_trail = false turns it off.
The base URL #
= "https://learn.peios.org"
Optional, and worth setting before you deploy. Without it:
- no
sitemap.xmlis written, because sitemaps require absolute URLs, androbots.txtgets noSitemap:line; - canonical URLs stay root-relative rather than absolute;
og:urlis omitted, since a relative one is meaningless to the crawlers reading it;/printbundles link outward relatively, which is right on the site and wrong on paper.
Trailing slashes are trimmed, so https://learn.peios.org/ and https://learn.peios.org behave identically.
Featured products #
= ["peios", "pekit", "provium", "universal-directory", "trail"]
The list does two jobs: it orders products, and it decides which appear on the front page.
- Products in the list sort first, in the list's order; everything else follows, by title.
- Only listed products get a card on the front page. An unlisted product is still built, still searchable, still linkable, and still in the header's products menu.
- Naming a product that does not exist is a build error.
Header navigation #
[[]]
= "Specs"
= "~peios/pgss"
[[]]
= "Source"
= "https://github.com/peios/peios"
Each [[nav]] block adds a link to the header, beside the products menu. On narrow screens they move inside that menu.
Keep the [[nav]] blocks at the end of the file: in TOML every key after a table header belongs to that table, so an ordinary key written below them becomes a nav field and fails the build.
url may be an external URL, a root-relative path, or a ~ page reference. References are resolved strictly, at load time — even with --allow-dangling-links, a broken nav link fails the build. Site chrome appears on every page; it must never dangle.
Favicon #
= "favicon.png"
A path relative to the site root. The file is copied to the output root under its own name and linked from every page. The file must exist, or the build fails.
Naming a file here is also what makes it legal to keep in the site root, which otherwise accepts only trail.toml and *.product directories.
Edit links #
= "https://github.com/peios/learn/edit/main/{path}"
Adds an "Edit this page" link to the foot of every article. {path} is replaced with the article's source file path relative to the site root — the placeholder is required, and a template without it is a build error.
For an alias page the link points at the original's source file, since that is the file to edit.
Injecting your own head HTML #
= "head.html"
A path to a file whose contents are injected verbatim at the end of every page's <head>: analytics snippets, extra meta tags, a webfont, a verification token.
It is inserted unescaped and unchecked, at the end of the head, so it can override earlier tags. It is the escape hatch for everything trail does not have a setting for — and, like all escape hatches, the thing to reach for last.
Files trail does not build #
= ["CNAME", ".well-known"]
Some files have to be in the published site without trail having any opinion about them: a CNAME for a custom domain, a .well-known/ directory, a search-engine verification file. passthrough names them, and each one is copied into the output exactly as it is — directories whole.
This matters most when the site root is a repository root. Trail accepts only trail.toml and *.product directories there, so a CNAME sitting beside them would otherwise fail the build. Naming it here makes it legal and publishes it.
Two details worth knowing:
- Entries are copied after everything trail generates, so naming
robots.txtreplaces the generated one — useful, and a foot-gun if you nameassets. - Dot-entries like
.github/are already skipped by the root scan, so they only need naming here if you want them published..well-known/does;.github/does not.
Theming #
Four keys control colour:
= "#3b82f6"
= "#7aa8ff" # optional; derived from accent if absent
= "custom.css" # optional
= true # default
accent is the site's colour. accent_dark overrides its dark-mode variant, which is otherwise derived by mixing the accent toward white. custom_css names a stylesheet shipped at /assets/custom.css and loaded after the built-in one. product_theming = false stops each product tinting its own pages.
Both colours must be #rrggbb, and setting accent_dark without accent is an error. Theming covers what these actually reach.
What may live in the site root #
The site root accepts trail.toml, *.product directories, the build output directory, dot-entries (.git and friends), and whatever favicon, custom_css, head_html and passthrough name. A setting naming assets/site.css makes the whole assets/ directory legal at the root — it is the first path component that is tolerated. Anything else is a build error.
That last clause is the reason those three settings take paths rather than inline content: naming a file is what makes it part of the site.
The output surface
Trail / Building
A build writes rather more than the pages you authored. This page is the full inventory of what lands in the output directory and what each part is for.
dist/
├── index.html the front page
├── 404.html the not-found page
├── robots.txt
├── sitemap.xml only when the site config has a `url`
├── llms.txt the site's map, for language models
├── site.json the site's structure, machine-readable
├── pekit.md a product, as markdown
├── pekit/
│ ├── index.html the product landing page
│ ├── print/index.html all of pekit on one page
│ ├── print.md all of pekit as one markdown file
│ └── reference/
│ ├── cli/index.html an article
│ ├── cli.md the same article, as markdown
│ ├── print/index.html all of the Reference topic on one page
│ └── print.md
├── assets/ style.css, search.js, fonts, licences
└── pagefind/ the search index and its WASM core
HTML pages #
One page per article, product, anthology and book cover, plus the front page, the 404 page and one /print page per unit. Each is written as <path>/index.html, so /pekit/reference/cli needs a host that serves directory indexes — nearly all of them do.
Topics, subfolders, chapters and shelves have no page. The count trail build prints is exactly this set.
Single-page views: /print #
Every unit — a product, an anthology, a topic or a book — also publishes its whole contents as one page:
/pekit/print every article in pekit, in reading order
/pekit/reference/print every article in the Reference topic
/peios/pgss/print the whole book
Each article becomes a section with its title, its breadcrumb, and a heading id unique within the bundle. The page is styled for reading and for printing: browser chrome, the site header and footer, the pager and the tab buttons all drop out when it goes to paper, and tab panels expand so nothing is hidden.
Links are rewritten so the document works away from the site: a link to a page inside the same bundle becomes an in-document anchor, and one outside it becomes an absolute URL when the site config has a url, staying root-relative otherwise.
Every article page carries a "Single page" link in its breadcrumbs, pointing at its unit's bundle anchored at that article — so a reader can go from a page to the whole document without losing their place.
Two things do not carry over into a bundle: inline reference phrases and § citations are not linked there. RFC-2119 keywords still are.
A unit with no articles gets no bundle.
Markdown mirrors #
Every page is also available as markdown at its own URL with .md appended.
/pekit/reference/cli → /pekit/reference/cli.md
/pekit → /pekit.md
/pekit/reference/print → /pekit/reference/print.md
A mirror is the article's source, not a re-derivation of the HTML — the same headings, the same fenced code blocks with their info strings, the same tables — with three rewrites:
~references become the target's own.mdURL, so following links keeps you in the markdown surface;- relative image destinations become the images' published URLs, since the mirror is served away from the article's folder;
- inside a book,
h2/h3headings gain their section numbers as text.
Each one opens with the title, the breadcrumb trail, and the description:
- Every HTML page also advertises its mirror in the head:
llms.txt #
A map of the site at the root, following the llms.txt convention: the sitename and description, a note that every page has a .md twin, and then one section per product listing its units by their print.md bundles.
# Peios Learn
> Concepts, guides, and reference for the Peios operating system…
Every page on this site is also available as markdown at the same URL with
`.md` appended. Each section below links whole units as single markdown
files. The full machine-readable structure is at /site.json.
## pekit
- [pekit overview](/pekit.md): The recipe-driven build and packaging tool…
- [All pekit docs in one file](/pekit/print.md)
- [Getting started](/pekit/getting-started/print.md): 2 articles
The --render-llms-full flag additionally writes each unit's print.md as llms-full.txt beside it, for agents that probe for that filename rather than reading llms.txt. It is off by default: it duplicates a large part of the output and adds nothing new.
site.json #
The whole site model as JSON: the site's own metadata, then products, and inside them the full tree of anthologies, topics, folders, books, chapters and articles.
Every node carries the URLs of its own alternative representations, so a consumer can walk the structure and fetch exactly the parts it wants. Book entries add number and, for appendices, appendix: true; alias pages add original.
sitemap.xml and robots.txt #
robots.txt is always written, allowing everything, with a Sitemap: line when the site config has a url.
sitemap.xml is written only when it has one, because sitemaps require absolute URLs. It lists every HTML page — the front page, products, anthologies, book covers, articles and /print bundles — each with a <lastmod> taken from that page's updated: date, or for a container, from the most recent one beneath it. Alias pages are left out: their canonical is the original.
The 404 page #
404.html is a real page in the site's theme, carrying <meta name="robots" content="noindex">. Trail writes it; your host has to be told to use it. trail serve already does, and returns a real 404 status with it.
Search #
/pagefind/ holds a Pagefind index built from the rendered pages: the index chunks, the WASM search core, and the pagefind.js module the theme's search script imports.
The front page is not indexed. Neither are alias pages — the original covers the same content — nor the parts of a page marked as chrome: breadcrumbs, the meta line, the Related Content list, the pager and the edit link. Each indexed page carries its breadcrumb trail as metadata, which is what search results show under the title.
Assets #
/assets/ holds the stylesheet, the search script, the two variable fonts with their licences, and LICENSE-Syntaxes.txt — the licences of the bundled syntax-highlighting definitions, which ask to travel with them. mermaid.min.js and its licence join them only if some article on the site contains a diagram. A configured custom_css ships as /assets/custom.css; a configured favicon ships at the output root under its own name.
Images are published at the URL of the directory they were found in — but only the ones some article actually references. See Images.
The cache-busting copy #
--cbpath <PATH> adds one more entry to the output: dist/<PATH>/, holding everything above a second time, with every link inside it rewritten to stay inside it. It is how you get a URL that no cache can have seen. See The cache-busting copy.
Passthrough files #
Anything named by the root config's passthrough is copied in verbatim — a CNAME, a .well-known/ directory, a verification file — keeping its path relative to the site root. It is written after everything else, so a passthrough named for a generated file (robots.txt) replaces it.
Passthrough files are tracked like any other output, so they survive the prune between builds.
The cache-busting copy
Trail / Building
Static hosts serve your pages with cache headers you do not control. GitHub Pages sets its own; so does every CDN in front of it. For the minutes or hours those headers claim, a reader — or, far more often, an automated fetcher — can be served the previous build with nothing to tell them apart.
--cbpath answers that by publishing a second, complete copy of the site under a path you name:
built 1227 pages (8 products) → ./dist
cache-busting copy → ./dist/8f3a91c/
Now /8f3a91c/ is the site's front page and /8f3a91c/pekit/reference/cli is that article. Name the copy after something that changes on every deploy — a commit hash — and every URL inside it has never been requested by anyone, so nothing anywhere can have it cached. What you get back is necessarily this build.
Why a copy and not a redirect #
The obvious cheap version — one busted URL that redirects, or a byte-for-byte copy of the tree — does not work, and fails in a way you cannot see.
Every URL trail emits is site-absolute: /pekit/reference/cli, /assets/style.css, /pagefind/pagefind.js. A plain copy at /8f3a91c/ would serve you a fresh HTML document for the one page you asked for, and that document would then load the cached stylesheet, the cached search script, and every link out of it would land back in the cached tree. One click and you are reading the old build again, with nothing on the page to say so.
So the copy is not copied. It is built, with the path baked into every URL it emits.
What carries the prefix #
| Every link between pages | breadcrumbs, sidebars, cards, the pager, ~ references in prose |
§ section citations and inline references | |
| Images that live in the tree | the ones trail publishes for you |
| Assets | the stylesheet, the search script, mermaid, your custom.css, the favicon |
| The search index | so a result inside the copy stays inside it |
| The whole markdown surface | .md mirrors, print.md, llms.txt, site.json — the copy is mostly for machine readers, so these matter most |
passthrough entries | copied into the copy as well as the site |
And what does not:
<link rel="canonical"> | names the real page, so the copy never competes with it |
| URLs you wrote out in full | https://…, and a leading-slash path you typed yourself — trail emits those exactly as written rather than second-guessing them |
edit_url | it points at your repository, not at the site |
robots.txt and sitemap.xml | see below |
Search engines #
The copy is a complete duplicate of the site at a path that changes on every deploy — exactly the thing a search engine should never index. Trail makes sure it isn't:
- every page in the copy carries
<meta name="robots" content="noindex, follow">; - every page names the real page as its canonical;
- the copy has no
sitemap.xml, and the site's sitemap does not mention it.
There is deliberately no Disallow in robots.txt. Some automated fetchers honour robots.txt on a URL you handed them directly, and blocking the copy would defeat the point of having it.
The path #
One plain segment, made of letters, digits, -, _ and ., not starting with a dot. It must not be a path the site already uses — a product slug, assets, pagefind, site.json, a passthrough entry — or the copy would bury it. Trail checks before it builds anything:
Error: --cbpath 'assets' is already a path in the site; the copy would bury it — pick a name the site does not use
What it costs #
The output roughly doubles: twice the pages, twice the mirrors, a second search index. On a large site that is real — Peios Learn goes from 115 MB to 232 MB, and its deploy artifact from 27 MB to 54 MB. Build time roughly doubles too. Nothing about the site itself gets slower; the copy is only ever fetched by someone who asks for it by name.
In a deploy #
The point is to inject the commit, so the path is new on every push:
- name: Build site
run: trail build --cbpath "${GITHUB_SHA::8}"
On a push, GITHUB_SHA is the commit that actions/checkout just checked out, so the path and the content it names cannot disagree. The first eight characters are plenty to be unique per deploy and short enough to paste by hand.
Each deploy replaces the output wholesale, so only the current commit's copy exists at any moment — the previous one is pruned along with everything else the build did not write. Do not bookmark one.
Finding it as a reader #
The site itself learns where its copy is, and the search box gets a command for it. Type /cb (or /cachebust) and you land on the same page you were reading, in the copy. Type it again from inside the copy and you come back out. See the reading experience.
Theming
Trail / Building
Trail ships one theme, compiled into the binary. There is no template directory to override and no theme to install. What you do control is colour — through configuration for the ordinary cases, and through CSS custom properties for everything else.
The three colour settings #
= "#3b82f6" # the site's colour
= "#7aa8ff" # optional; derived from accent if absent
= true # default
accent is the site's colour: links, focus rings, the wordmark's accented word, the hero glow, the front-page cards. Without it the theme's own default is used.
accent_dark is the dark-mode variant. Without it, trail derives one by mixing the accent 75% toward white — a light accent reads badly on a dark background, and most brand colours need lifting. Set it explicitly when the derived one is wrong. Setting it without accent is a build error.
Both must be #rrggbb.
Each product also has a color, and by default it displaces the site accent on every page belonging to that product: cards, the monogram tile, sidebar highlights, heading numbers, RFC-2119 keywords, focus rings. product_theming = false turns this off site-wide — every per-product tint collapses to the site accent, and products keep only their monogram and their card.
Three-state light and dark #
Every reader is in one of three states, and the header toggle cycles between them:
| State | |
|---|---|
| System | Follow the operating system's preference. The default. |
| Light | Pinned light, whatever the system says. |
| Dark | Pinned dark, whatever the system says. |
The choice is stored per reader in their browser and applied before first paint, so a reader who has chosen dark never gets a flash of white.
In CSS this is three blocks: :root carries the light tokens; :root[data-theme="dark"] overrides them when the toggle has pinned dark; and @media (prefers-color-scheme: dark) :root:not([data-theme="light"]) overrides them for system-dark readers who have not pinned light.
The tokens #
Everything resolves through CSS custom properties, so overriding a token restyles every place it is used.
| Token | |
|---|---|
--bg | Page background. |
--surface | Cards, menus, the sidebar — translucent over the background. |
--text | Body text. |
--text-muted | Secondary text: crumbs, meta lines, captions. |
--border, --border-strong | Hairlines and emphasised edges. |
--accent | The site accent, as resolved for the current theme. |
--pc | The product colour in scope. Falls back to --accent. |
--glow, --glow-mix | The accent glow behind heroes. |
--headline | The gradient headings are painted with. |
--tile-mix, --tile-fg-mix | How strongly monogram tiles take their product colour. |
--card-shadow | Card elevation. |
--menu-bg | The products dropdown, which must be opaque. |
--code-comment, --code-keyword, --code-string, --code-constant, --code-function, --code-type | The syntax-highlighting palette. |
--pc is the one to know. It is set inline on every element scoped to a product, which is how one stylesheet tints five products differently — and why product_theming = false can collapse them all with a single rule.
Syntax highlighting follows the theme #
Code is highlighted at build time into class names, not inline colours: hl-comment, hl-keyword, hl-string and so on. Those classes resolve through the six --code-* tokens above, which are redefined in each theme state — which is why code goes light and dark with the rest of the page. Override those six tokens to change the code palette; see Code blocks.
Fonts #
Two variable fonts ship with the site and are served from /assets/fonts/: Inter for body text and Space Grotesk for headings. Both are self-hosted — no third-party font request — and their licences ship beside them, as the SIL Open Font License requires.
To use different fonts, add @font-face rules and a font-family override in your custom stylesheet. The built-in fonts are still emitted; there is no setting to suppress them.
The custom stylesheet #
= "custom.css"
The named file ships at /assets/custom.css and is loaded after the built-in stylesheet, on every page.
Overriding tokens goes furthest, and survives changes to trail's own CSS:
}
}
{
}
}
Beyond that it is ordinary CSS against trail's own class names — .card, .sidebar, .admonition, .code-block and the rest. Those class names are not a stable interface: they are what the compiled templates happen to emit today, and a trail upgrade may change them. Token overrides are the durable half.
Injecting into the head #
For anything that is not a stylesheet — a webfont link, an analytics snippet, extra meta tags — head_html names a file injected verbatim at the end of every page's <head>.
What you cannot change #
- The templates. They are compiled into the binary and are not user-replaceable. Page structure is trail's.
- The layout, beyond what CSS can reach.
- The set of page types. There is no way to add a new kind of page.
That is the trade trail makes: no plugin system and no theme directory, in exchange for a single binary with nothing to install and one appearance to maintain.
The reading experience
Trail / Building
None of this needs configuring. It is what every trail site does, and it is here so you know what your readers have — and so you can write pages that make use of it.
Search #
⌘K — or Ctrl K, and the pills in the header say which, based on the reader's platform — opens a search modal from any page. So does clicking the search box in the header or the one on the front page.
Search is powered by a Pagefind index built at build time. The engine and its index chunks load lazily, on the first open, so a reader who never searches pays nothing for it.
- Results show the page title, its breadcrumb trail, and an excerpt with the matches highlighted.
- Up to eight results are shown; the status line says how many more there were.
- ↑ and ↓ move; Enter opens; Esc or a click on the backdrop closes.
- The query is carried through to the page, which highlights every occurrence of each term in the article body and scrolls the first into view. Matches inside code blocks and breadcrumbs are skipped.
What is indexed is the article body. Chrome — breadcrumbs, the meta line, Related Content, the pager, the edit link — is excluded, and so is the front page and every alias page.
Commands #
A query beginning with / is a command rather than a search. Typing / on its own lists the ones this page has; typing more narrows the list, and Enter runs the highlighted one.
| Command | |
|---|---|
/theme | Cycles system → light → dark, exactly as the header button does. |
/theme <name> | Goes straight to system, light or dark. |
/cachebust, /cb | Opens the page you are on in the cache-busting copy — a URL nothing has cached. From inside the copy it takes you back out to the real page. Offered only when the build published a copy. |
Nothing needs enabling. The commands a page offers are the ones that can do something there, which is why /cb appears on a site built with --cbpath and nowhere else.
Light, dark and system #
The header's theme button cycles system → light → dark, and its icon shows which state is active. The choice is remembered in the reader's browser and applied before first paint, so there is no flash of the wrong theme.
"System" is the default and follows the operating system's preference live. See Theming.
Navigation #
The sidebar on an article page shows the article's own topic — its title above its contents in reading order, subfolders as collapsible groups (expanded automatically around the current page), and the current article marked. In a book it is the whole book as a tree with section numbers, chapters collapsible, and the trail to the current page opened. Chapters a reader opens by hand stay open as they move through the book, for that tab.
"On this page" lists the article's h2 and h3 headings in a right-hand column, and highlights the one currently in view as the reader scrolls. When the window is too narrow for a third column it moves into the sidebar. An article with no headings gets no list.
Every heading has a permalink — a # that appears on hover and links to that heading's stable id.
Previous and next links at the foot of an article step through the whole topic, or the whole book, in reading order.
Breadcrumbs run from the product down through anthologies, the topic and any subfolder.
A back-to-top button appears once the page has been scrolled past about 600 pixels.
Text size #
Three buttons at the top of the right-hand column set the prose to small, medium or large. The choice is remembered per reader.
It scales the article text only — the sidebar, header and chrome stay put. Making the whole page bigger costs a reader half their screen; making the prose bigger is what they actually wanted. On narrow screens the controls ride into the drawer with the rest of the sidebar.
Code and content #
- Every code block has a copy button, top-right, appearing on hover and always visible on touch devices. It says "Copied" briefly, and falls back to telling the reader to press ⌘C if the browser refuses clipboard access.
- Syntax highlighting follows the theme, because it is class-based rather than baked-in colours.
- Tables scroll inside their own container, so a wide table never makes the whole page scroll sideways.
- Images carry their real dimensions, so text does not jump around as they load.
- Tab groups keep one panel open at a time, independently per group.
- Mermaid diagrams are drawn in the reader's theme, and the library is only shipped to pages that contain one.
Page furniture #
A meta line under the title gives the estimated reading time and, when the article has an updated: date, when it last changed. Containers total theirs up, so a book cover says how long the whole book takes.
A "Single page" link in the breadcrumbs opens the whole topic or book as one page, anchored at the article the reader was on. It is the right link for someone who wants to read the lot, print it, or search the whole document at once. See The output surface.
"Edit this page" appears at the foot of every article when the site config sets edit_url, pointing at the article's source file.
A "Related Content" list appears above the pager when the article's frontmatter has a related: list.
On small screens #
The sidebar becomes a drawer, opened by the menu button in the header and closed by the overlay, the button again, or following any link inside it. "On this page" and the text-size controls ride in with it, so nothing is lost on a phone.
On paper #
Every page prints. The header, footer, search modal, pager, "Single page" link and back-to-top button all drop out; tab groups expand so every panel is visible, each labelled with its tab's name.
For printing a whole topic or book, use its /print view rather than printing pages one at a time.
Without JavaScript #
The pages are static HTML and read fine with scripts disabled. What is lost is the interactive layer: search, the theme toggle, text size, copy buttons, tab switching, the drawer, scroll-spying and diagram rendering. Content, navigation links, the sidebar tree and every ~link work regardless.