Wheelhouse docs

Page template and Markdown reference#

This page is the specification and the demonstration at once. It is written in the same Markdown every other page uses and rendered by the same code, so nothing here can describe a feature the renderer does not have. Copy it when you start a page, delete what you do not need.

Front matter#

Every page opens with a fenced block of key: value lines. It is not YAML — the parser is about forty lines in build.py and understands scalars, [a, b] lists, integers and booleans, nothing more.

markdown
---
title: Firewall
description: Zone policy, rule sets, and the nftables they become.
order: 20
tags: [firewall, nftables]
updated: 2026-09-02
---
KeyTypeDefaultWhat it does
titletextThe <h1> in the browser tab, the navigation label, the search result. Required; the build fails without it.
descriptiontextThe <meta name="description"> and the search snippet. Omitting it is a warning.
orderinteger500Position among siblings in the navigation tree. Ties break alphabetically.
nav_titletexttitleA shorter label for the tree, when the full title is a mouthful.
tagslist[]Shown at the foot of the page and weighted in search.
updatedtextPrinted at the foot. Write the date you checked the claims, not the date you touched a typo.
layouttextpageAdds layout-<name> to the <body> class. The sales site uses home.
draftboolfalseSkips the file entirely. It is not built, not linked, not indexed.
noindexboolfalseBuilds the page but keeps it out of sitemap.xml and sets a noindex robots tag.
navbooltruefalse keeps the page out of the navigation tree, and therefore out of the previous/next chain.
tocbooltruefalse suppresses the on-this-page column.
breadcrumbs, prevnextbooltruefalse suppresses those.
searchbooltruefalse keeps the page out of the search index.
redirect_tolinkTurns the page into a redirect stub. See below.

Moving a page#

A published path is a promise, and a static host cannot answer a 301. So a page that moves leaves its old path behind as a stub: front matter with redirect_to and nothing else.

markdown
---
title: Firewall rules
description: Moved to /manual/firewall/rules/.
redirect_to: firewall/rules.md
---

The build emits a <meta http-equiv="refresh">, a canonical URL pointing at the new home, a visible link for a reader whose browser blocks the refresh, and keeps the stub out of the sitemap, the search index and the navigation tree. redirect_to takes the same link forms as everything else, so it is checked like everything else.

Where the file goes#

The path under content/ is the URL. Relative links and images resolve against the source file's directory, so a link written in the Markdown is the link that works.

How a Markdown file becomes a URL

Text#

Ordinary paragraphs. Bold for the load-bearing clause of a sentence, italic sparingly, and inline code for anything the reader will type or that came off the router: a flag like --api-key-file, a path like /config/wheelhouse/state.json, a header like X-Wheelhouse-CSRF.

Two trailing spaces force a line break —
like that — which is worth exactly one use per page, in an address block.

Escaping works the way you would hope: a literal asterisk is \*, and any HTML you write in a Markdown file is escaped rather than rendered. Writing <script> in a sentence produces the four characters <script>, not a security incident. That is not politeness; it is the reason the renderer is a scanner that escapes everything it does not recognise instead of a set of regular expressions run over the whole document.

WrittenResolves to
[Contributing](index.md)the sibling page, /contributing/
[Firewall](../manual/firewall.md)a page in another section
[this section](#links)an anchor on this page; the build checks the heading exists
[the product site](www:/)the sales site's front page
[the manual](wiki:/)the wiki's front page
[downloads](releases:/latest/)the release host
[the source](forge:/)the git forge
[agent/license.go](repo:agent/license.go)that file in the forge, and the build fails if it is not in the tree
<https://vyos.io/>an autolink, marked as leaving the site

The last one is the rule that keeps this site honest. Cite the file you read:

The agent verifies a licence key offline against a public key compiled into the binary, so a router with no route to the internet still knows what it holds — agent/license.go.

A link may carry one class from a short allowlist, which is how the sales site gets its buttons: [Download](releases:/latest/){.button}. Unknown class names are dropped with a warning rather than passed through into the HTML.

Lists#

Unordered, nested by indentation:

  • The agent drives VyOS over its documented HTTP API.
    • The config plane stages set and delete lines and commits them as one diff.
    • The Linux plane reaches beneath the config tree where the tree cannot express something:
      • tc for the full CAKE knob set,
      • companion daemons for split-horizon DNS and PXE.
  • Everything out-of-tree is declared in one file and reconciled idempotently.

Ordered, with a nested unordered list and a code block inside an item:

  1. Sign in at https://<router>:8443 as admin.
  2. Change the password. The account is flagged must change until you do.
  3. Turn on two-factor authentication, then issue a token for anything automated:

    bash
    curl -sk -X POST https://router:8443/api/tokens \
      -H "X-Wheelhouse-CSRF: $CSRF" -b cookies.txt \
      -d '{"name":"terraform","role":"operator"}'
  4. Stage a change and look at the diff before you commit it.

Code#

A fenced block takes a language, and optionally a title that replaces the language label in the header bar. The copy button appears only when JavaScript is running, because a button that does nothing is worse than no button.

markdown
```yaml title="/config/wheelhouse/agent.yaml"
qos:
  wan:
    iface: eth1
```

Which renders as:

/config/wheelhouse/agent.yaml
qos:
  wan:
    iface: eth1
    egress: { qdisc: cake, bandwidth: 70mbit, docsis: true, ack-filter: true }

With no title, the language is the label:

console
$ wheelhouse-agent plan --file /config/wheelhouse/agent.yaml

There is no syntax highlighting. It would cost a runtime dependency or a hand-written lexer per language, and neither buys enough to be worth the weight on a page that has to stay under 150 KB.

Tables#

Alignment markers work: :--- left, :---: centre, ---: right. Wide tables scroll inside their own box rather than pushing the page sideways.

FlagDefaultMeaning
--addr127.0.0.1:8090Listen address. The shipped systemd unit overrides it to 0.0.0.0:8443.
--data-dir/config/wheelhousePersistent state. On VyOS, /config survives an image upgrade and /var/lib does not.
--api-key-fileFile holding the VyOS API key. Preferred over --api-key, which is visible in ps.
--tls-self-signedfalseGenerate a certificate in the data directory if none exists.

Every row above is from agent/main.go. That is the standard: if you cannot point at the line, do not write the row.

Blockquotes#

A quotation, for something said elsewhere that you are reporting rather than asserting.

It can hold more than one paragraph, and code.

Admonitions#

Written as a ::: fence. The word after the colons is the type; anything after that is an optional title replacing the default one.

TypeDefault titleUse it for
noteNoteA fact worth pulling out of the prose.
tipTipSomething that makes the reader's life easier.
warningWarningThis can cut you off from the router, erase a disk or expose the management UI. Sparingly, so it keeps meaning something.
dangerDangerIt will break, and here is exactly how.
roadmapNot built yetPlanned, not shipped. The words are in the text, not only the styling.
platform-gap (or gap)Platform gapVyOS itself cannot express this, and what Wheelhouse does about it. A fact with a workaround, never a complaint about upstream.
markdown
:::warning Read this before you commit
Body text, with the **full** Markdown available.
:::

Horizontal rule#

Three or more dashes on their own line. Use it to separate an appendix from the body, not to decorate.


What the build checks for you#

  1. Every internal link resolves to a page that exists.
  2. Every #fragment names a heading that is really on the target page.
  3. Every repo: path exists in this checkout.
  4. Every page has a title.
  5. Every page's total weight — HTML, CSS, JavaScript and the one self-hosted font — stays under the 150 KB budget, and the build names the heaviest three pages per site so you can watch the number move.

Numbers 1 through 4 fail the build. Number 5 warns, and fails under --strict.

Accessibility, briefly#

Headings descend by one level at a time; the anchor beside each one is a real link with a label. Tables have header cells. Images have alt text that says what the image shows, not "diagram". Colour is never the only signal — an admonition carries its type as a word, not just as a stripe. The page reads and navigates with no JavaScript at all; search is the single feature that needs it, and it says so when it is missing.

Updated 2026-09-02 meta markdown