[{"content":"This demo runs in English and German. Not English with a German veneer: each language has its own content directory, its own menu, its own search index, its own feed, and its own set of tags. A post may exist in one language and not the other, which is the normal case rather than an omission to be apologised for.\nThe shape of it defaultContentLanguage = \u0026#34;en\u0026#34; defaultContentLanguageInSubdir = true [languages.en] languageName = \u0026#34;English\u0026#34; contentDir = \u0026#34;content/en\u0026#34; weight = 1 [languages.de] languageName = \u0026#34;Deutsch\u0026#34; contentDir = \u0026#34;content/de\u0026#34; weight = 2 defaultContentLanguageInSubdir puts every language under a prefix, English included. Nothing lands at the publish root — which is exactly why the theme ships a ROOT404 output format, so the web server still has an error document to serve.\nImportant\nAdd ROOT404 to the default language only. Give it to both and they fight over /404.html, with the winner decided by build order.\nPairing translations Two posts become translations of each other by sharing a translationKey, which frees their filenames and their URLs to be idiomatic in each language:\ntranslationKey = \u0026#34;multilingual-by-design\u0026#34; The title block then grows an Also in row, and the header\u0026rsquo;s language switch points at this page\u0026rsquo;s counterpart rather than dumping the reader on the home page. Where there is no counterpart — as on two of the posts in this demo — the switch falls back to that language\u0026rsquo;s home page, which is the honest answer.\nPer-language parameters Anything under [languages.xx.params] reaches templates as site.Params, already resolved for the current language. That is what makes a genuinely per-language editPost link possible:\n[languages.de.params.editPost] URL = \u0026#34;https://example.org/-/edit/main/content/de\u0026#34; appendFilePath = true .File.Path is relative to that language\u0026rsquo;s contentDir, so a single site-wide URL could only ever have been correct for one of the two.\nDates Set DateFormat = \u0026quot;:date_long\u0026quot; rather than a Go layout string. A layout like January 2, 2006 freezes the month name in one language and will cheerfully print January on a German page; the shorthand resolves per language.","permalink":"/felix/vellum/en/posts/multilingual-by-design/","summary":"Two languages, neither of them a translation layer over the other — and what that costs in configuration.","title":"Multilingual by Design"},{"content":"A drawing frame is divided into zones — letters down one edge, numbers along the other — so that two people on a phone call can agree on where they are looking. \u0026ldquo;The bracket in C4.\u0026rdquo; It is a coordinate system for a conversation.\nWhat it does here This theme keeps the letters and gives them a job: one zone per top-level heading, in document order, each one a link. The rail you can see down the left edge of this page has three letters because this post has three ## headings. Scroll, and the current one is marked.\nThat makes the rail a table of contents that costs no vertical space — it lives in the frame margin, which was otherwise empty ruling.\nWhen it does not render This is the part worth stating plainly, because it is where the metaphor could have gone wrong: a page with nothing to index gets no rail at all.\nA profile or landing page has no ## headings, so no rail. A short note written as one continuous argument has none either — and that is a legitimate way to write, not a defect to route around. Lettering an empty field would be a drawing that claims a structure it does not have. An empty zone rail is worse than no zone rail.\nNote\nThe separate ShowToc block is a different thing: a conventional, collapsible contents list that includes ### headings too. The rail is the coarse index; the ToC is the fine one. A post can have both, either, or neither.\nHeadings and anchors Every heading gets an anchor link, revealed on hover and on keyboard focus:\n## When it does not render The render hook that adds it lives in layouts/_markup/render-heading.html. Because it is a render hook rather than JavaScript, the anchors exist in the HTML as shipped — they work with scripting off, and they survive being saved to a file.","permalink":"/felix/vellum/en/posts/the-zone-rail/","summary":"On a real drawing the border zones let two people talk about the same spot. Here they index the post.","title":"The Zone Rail"},{"content":"The reading column in this theme is 92 characters wide for one reason: an 80-column terminal paste should land in it without wrapping. Everything else about how code is set follows from that decision.\nHighlighting Chroma emits class names rather than inline styles, which is what lets the syntax colours resolve through the same light-dark() tokens as the rest of the page. The consuming site has to opt in:\npygmentsUseClasses = true [markup.highlight] noClasses = false Without both, the code blocks ship a light-mode palette baked into the HTML and stay light on a dark page.\nA worked example // tokens.html parses four values out of 00-tokens.css at build time, because a // sizes attribute is evaluated with no element context and cannot use var(). func measure(css string) (int, error) { m := regexp.MustCompile(`--content-width:\\s*(\\d+)px`).FindStringSubmatch(css) if m == nil { return 0, fmt.Errorf(\u0026#34;--content-width not found: the layout and the \u0026#34;+ \u0026#34;responsive images would silently disagree\u0026#34;) } return strconv.Atoi(m[1]) } Note the failure mode named in the error string. A missing token here would not crash anything — it would quietly make every image advertise the wrong sizes, which is the kind of bug that survives for months.\nLong output An 80-column terminal paste, unwrapped:\n$ hugo --source exampleSite --themesDir ../.. --printPathWarnings Start building sites … hugo v0.165.0+extended linux/amd64 BuildDate=2026-08-12T14:26:28Z │ EN │ DE ───────────────────┼────┼──── Pages │ 24 │ 21 Paginator pages │ 2 │ 1 Non-page files │ 4 │ 2 Processed images │ 6 │ 6 Aliases │ 4 │ 3 Cleaned │ 0 │ 0 Total in 284 ms When output runs to hundreds of lines, fold it with the collapse shortcode rather than trimming it — a truncated log is the one thing a reader cannot reconstruct.\nCopy buttons ShowCodeCopyButtons = true puts a copy control on every block. It appears on hover and on focus, so it is reachable from the keyboard, and it is suppressed in print — a paper copy has nothing to copy to.\nTip\nInline code like --themesDir ../.. is set in the same face as the blocks, one step down. It is deliberately not boxed: a box on every inline span turns a paragraph into a fence.","permalink":"/felix/vellum/en/posts/code-and-terminal-output/","summary":"Fenced blocks are the substance of a technical post, not an inset in it — so they get first-class treatment.","title":"Code and Terminal Output"},{"content":"Goldmark\u0026rsquo;s unsafe setting is off in this theme, so raw HTML written in a post is stripped rather than rendered. That is the right default — a content file should not be able to smuggle a script into the page — but it removes a few things that technical writing genuinely needs. The shortcodes below are exactly those things, and nothing more.\ncollapse The load-bearing one. Long command output should be present — searchable, printable, copyable — without occupying half the screen on the way past.\nFull build output $ hugo --gc --minify --printPathWarnings Start building sites … hugo v0.165.0+extended linux/amd64 │ EN │ DE ──────────────────┼────┼──── Pages │ 24 │ 21 Paginator pages │ 2 │ 1 Non-page files │ 4 │ 2 Static files │ 0 │ 0 Processed images │ 6 │ 6 Aliases │ 4 │ 3 Total in 284 ms Written as:\n{{\u0026lt; collapse summary=\u0026#34;Full build output\u0026#34; \u0026gt;}} ```console … ``` {{\u0026lt; /collapse \u0026gt;}} details is an alias of the same shortcode, under the name most people reach for first. Both take openByDefault=\u0026quot;true\u0026quot; if the fold should start open.\nfigure The image render hook resizes bundled images and gives them intrinsic dimensions, but it cannot produce a caption. That is the only reason this shortcode exists.\nA caption is the whole point. Without one, plain Markdown image syntax does the same job. {{\u0026lt; figure src=\u0026#34;shot.png\u0026#34; alt=\u0026#34;…\u0026#34; caption=\u0026#34;…\u0026#34; link=\u0026#34;…\u0026#34; \u0026gt;}} audio Takes a bundle resource or a plain URL. Nothing autoplays, here or in video — a reader who is mid-task with a terminal open does not want sound.\nvideo Same shape, plus an optional poster frame:\n{{\u0026lt; video src=\u0026#34;clip.mp4\u0026#34; poster=\u0026#34;still.png\u0026#34; muted=\u0026#34;true\u0026#34; loop=\u0026#34;true\u0026#34; \u0026gt;}} This demo ships no video file, so the shortcode is documented here rather than exercised.\nintextimg An image set into a run of text and sized to the line rather than to the column — a glyph, a badge, a status mark. The mark sits on the baseline of this sentence at 1.1em, and moves with the type when the reader changes size.\nai Marks a passage as written with AI assistance, using the notation a drawing already has for an altered region: a change bar down the edge, a numbered revision flag on the bar, and the number resolving to a row in the revision note at the foot of the sheet. The flag is a link — a reference that does not resolve is not a reference.\n1\nA whole paragraph can be marked. The bar is dashed rather than solid, which is what separates it from a block quotation in the same ink and what a drawing uses for a provisional line. Nothing here is a colour: the mark reads the same in greyscale, on paper, and to a reader who cannot separate the two inks.\nA run inside a sentence takes display=\u0026quot;inline\u0026quot; instead, so that only the words that warrant it2 carry the mark rather than the whole paragraph around them.\n{{\u0026lt; ai note=\u0026#34;Drafted from an outline\u0026#34; \u0026gt;}} A whole paragraph. {{\u0026lt; /ai \u0026gt;}} … a sentence with {{\u0026lt; ai display=\u0026#34;inline\u0026#34; \u0026gt;}}this run{{\u0026lt; /ai \u0026gt;}} in it. The post-level counterpart is front matter, not a shortcode: ai = true puts a stamp in the corner of the title block and the same mark in the index. This post carries no such stamp on purpose — individual passages are marked, the sheet as a whole is not.\nrawhtml A deliberate hole in unsafe = false. Everything inside is trusted exactly as far as the person who wrote the post is, which is why it is the last one listed and should be the last one reached for.\nCaution\nIf a render hook or another shortcode can do the job, use that instead. rawhtml gives up the guarantee that a content file cannot inject markup.","permalink":"/felix/vellum/en/posts/writing-with-shortcodes/","summary":"Seven shortcodes, and why a theme whose Markdown is otherwise plain needs any at all.","title":"Writing with Shortcodes"},{"content":"Every page in this theme is a drawing sheet. The ruled block under the title is the title block: on a real drawing it states what the sheet is, who drew it, when, and at which revision. Here it carries the same fields, as an actual bordered table rather than a grey caption line that the eye skips on its way to the first paragraph.\nWhat the title block records Each row is fed by one front matter key, and a row that has nothing to say is not ruled at all — an empty field on a drawing is a defect, not a placeholder.\nThe post you are reading has a lastmod more than a day after its date, which is why it shows a Revised row. Remove that key and the row disappears; the block closes up around it.\nField Source Shown when Issued date always Revised Lastmod it differs from date by more than a day Extent reading time, word count ShowReadingTime / ShowWordCount By author the key is set Subject tags the post has tags Also in translations the post exists in another language The reading column The column runs to 92 characters at the 20px body size, well past the classic 65–75 advice. That is a deliberate trade for technical writing, where the substance is command blocks and terminal output that should not wrap:\n$ hugo --gc --minify Start building sites … │ EN │ DE ──────────────────┼────┼──── Pages │ 24 │ 21 Processed images │ 6 │ 6 Total in 211 ms The line height carries the cost of that width: at 1.75 a 92-character line stays trackable, and the return sweep still lands on the right row. Both numbers were measured from a rendered line rather than estimated — a characters-per-pixel guess is reliably wrong.\nNote\nWritten as a plain Markdown blockquote opening with [!NOTE]. No shortcode and no raw HTML, which matters because Goldmark\u0026rsquo;s unsafe is off.\nThe zone rail The lettered strip down the left edge of the frame is not decoration. Each letter is one top-level heading in this post, and each is a link. On a page with nothing to index — a profile page, a short note — the rail does not render, because lettering an empty field would be a lie about the drawing.\nTip\nThe rail follows ## headings only. If a post reads as one continuous argument, give it no sections and it gets no rail.\nAlerts Five kinds, all plain Markdown:\nImportant\nNotes and tips carry the signal amber; warnings and cautions carry the danger colour. Only one accent is ever live on a screen at a time.\nWarning\nA wide table scrolls inside its own container rather than pushing the page sideways. Try the one above on a narrow window.\nCaution\nColour is never the only carrier: every alert is labelled in words as well.","permalink":"/felix/vellum/en/posts/reading-a-sheet/","summary":"What the title block records, why the reading column is wider than the usual advice, and how the sheet decides what to draw.","title":"Reading a Sheet"}]