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.
---
title: Firewall
description: Zone policy, rule sets, and the nftables they become.
order: 20
tags: [firewall, nftables]
updated: 2026-09-02
---| Key | Type | Default | What it does |
|---|---|---|---|
title | text | — | The <h1> in the browser tab, the navigation label, the search result. Required; the build fails without it. |
description | text | — | The <meta name="description"> and the search snippet. Omitting it is a warning. |
order | integer | 500 | Position among siblings in the navigation tree. Ties break alphabetically. |
nav_title | text | title | A shorter label for the tree, when the full title is a mouthful. |
tags | list | [] | Shown at the foot of the page and weighted in search. |
updated | text | — | Printed at the foot. Write the date you checked the claims, not the date you touched a typo. |
layout | text | page | Adds layout-<name> to the <body> class. The sales site uses home. |
draft | bool | false | Skips the file entirely. It is not built, not linked, not indexed. |
noindex | bool | false | Builds the page but keeps it out of sitemap.xml and sets a noindex robots tag. |
nav | bool | true | false keeps the page out of the navigation tree, and therefore out of the previous/next chain. |
toc | bool | true | false suppresses the on-this-page column. |
breadcrumbs, prevnext | bool | true | false suppresses those. |
search | bool | true | false keeps the page out of the search index. |
redirect_to | link | — | Turns 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.
---
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.
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.
Links#
| Written | Resolves 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
setanddeletelines and commits them as one diff. - The Linux plane reaches beneath the config tree where the tree cannot express
something:
tcfor the full CAKE knob set,- companion daemons for split-horizon DNS and PXE.
- The config plane stages
- 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:
- Sign in at
https://<router>:8443asadmin. - Change the password. The account is flagged must change until you do.
Turn on two-factor authentication, then issue a token for anything automated:
bashcurl -sk -X POST https://router:8443/api/tokens \ -H "X-Wheelhouse-CSRF: $CSRF" -b cookies.txt \ -d '{"name":"terraform","role":"operator"}'- 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.
```yaml title="/config/wheelhouse/agent.yaml"
qos:
wan:
iface: eth1
```Which renders as:
qos:
wan:
iface: eth1
egress: { qdisc: cake, bandwidth: 70mbit, docsis: true, ack-filter: true }With no title, the language is the label:
$ wheelhouse-agent plan --file /config/wheelhouse/agent.yamlThere 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.
| Flag | Default | Meaning |
|---|---|---|
--addr | 127.0.0.1:8090 | Listen address. The shipped systemd unit overrides it to 0.0.0.0:8443. |
--data-dir | /config/wheelhouse | Persistent state. On VyOS, /config survives an image upgrade and /var/lib does not. |
--api-key-file | — | File holding the VyOS API key. Preferred over --api-key, which is visible in ps. |
--tls-self-signed | false | Generate 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.
| Type | Default title | Use it for |
|---|---|---|
note | Note | A fact worth pulling out of the prose. |
tip | Tip | Something that makes the reader's life easier. |
warning | Warning | This can cut you off from the router, erase a disk or expose the management UI. Sparingly, so it keeps meaning something. |
danger | Danger | It will break, and here is exactly how. |
roadmap | Not built yet | Planned, not shipped. The words are in the text, not only the styling. |
platform-gap (or gap) | Platform gap | VyOS itself cannot express this, and what Wheelhouse does about it. A fact with a workaround, never a complaint about upstream. |
:::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#
- Every internal link resolves to a page that exists.
- Every
#fragmentnames a heading that is really on the target page. - Every
repo:path exists in this checkout. - Every page has a title.
- 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.