tilder, Documentation, Reference, The text mirror

Documentation

Name

text-mirror - every page as text, for terminals

Every page tilder builds comes out twice: as HTML for browsers, and as text for terminals, from the same source, so the two cannot drift apart. The text is 75 columns wide and folded to ASCII. It is written twice too: plain in txt/, and coloured with ANSI escape sequences in ansi/. A terminal that asks the site for a page gets the coloured one.

Where it is

Each page's text sits under txt/ and ansi/, at the page's address with .txt. A folder's page is named after the folder.

Page Plain Coloured
index.htmltxt/index.txtansi/index.txt
about.htmltxt/about.txtansi/about.txt
blog/index.htmltxt/blog.txtansi/blog.txt
blog/2026-01-01-hello.htmltxt/blog/2026-01-01-hello.txtansi/blog/2026-01-01-hello.txt
fr/about.htmltxt/fr/about.txtansi/fr/about.txt

Served by the example Caddyfile, curl example.org/about returns ansi/about.txt, with a 200 and no redirect; ?plain returns txt/about.txt instead, to save or pipe; and the plain-text host serves txt/ to every client, at the same paths (deployment). The files are also there to read from the disk:

less -R public/ansi/about.txt
curl example.org/about
curl "example.org/about?plain" > about.txt

robots.txt keeps search engines out of /txt/ and /ansi/, and txt/robots.txt out of the whole plain-text host: the text duplicates the pages (SEO).

The page as text

The text follows the page's sections, in the manner of a man page (shortened here: the real rules are 75 columns wide):

MYSITE(1)                 My Site Manual                 MYSITE(1)

LANGUAGES: en fr

NAME
     about - who we are

     A paragraph, wrapped at 75 columns, indented five spaces.

SEE ALSO
     ...

previous: Getting started                         next: Deployment

MYSITE                    2026-01-01                     MYSITE(1)
  • The header rule: the page's man: on both sides, site.manual in the middle.
  • On a site with several languages, a LANGUAGES: line lists them.
  • Each ## section: its title in capitals at the first column, its body indented five spaces.
  • Before the footer, the line of a sequential collection's neighbours, from labels.prev and labels.next (navigation).
  • The footer rule: footer.left, site.updated, footer.right (configuration).

What is left out: anything before the first ##, a section marked {html}, and a comment block. A section marked {text} appears only here, not on the page (sections). Trailing spaces are dropped, and never more than one blank line follows another.

75 columns

Every line fits in 75 columns: an 80-column terminal, the default since the VT100, with room left for a scrollbar, the gutter of less or a diff, or the > of a quoted email. Paragraphs are wrapped on spaces, greedily. A word is never cut, a URL or a command neither, so a single word longer than the line is the one thing that can pass the edge.

ASCII

txt/ is ASCII: the source keeps its accents and typography, the mirror folds them, whatever they sit in - the man header, the updated date, a member's link and an image's path are folded too. First these characters are replaced:

Character Becomes
em dash, en dash-
curly single quotes'
curly double quotes, guillemets"
middle dot, bullet-
ellipsis...
right arrow, double right arrow->, =>
left arrow, up arrow, down arrow, left-right arrow<-, ^, v, <->
less-or-equal, greater-or-equal, not equal<=, >=, !=
north-east arrow, U+2197nothing
the ligatures of o and e, a and e, in either caseoe, OE, ae, AE
eszett, ßss
o, l, d with a stroke, in either caseo/O, l/L, d/D
no-break space, narrow no-break spacea space
multiplication signx

Then every letter loses its accents and other marks: an e with an acute accent becomes e, a c with a cedilla becomes c. In prose, runs of spaces become one; in code, spacing is kept exactly. Whatever is left that is not ASCII - an emoji, another script, a symbol not in the table - is dropped: txt/ is ASCII only, whatever the source holds.

Blocks as text

Block In the text mirror
paragraphwrapped; its classes, {small muted}, affect the page only
list- item, 1. item, - [x] item, - [ ] item; wrapped lines and nested lists align under the item's text
quoteindented two more spaces
callouta box, its label in the top rule: +- WARNING ---+, from labels.info, labels.warning or labels.error
code blockframed by two rules, see below
tablepadded columns, like column -t; if they do not fit in 75 columns, one record per row, a Header: value line per cell
horizontal rulea line of dashes
image[ image ] alt text, from labels.image, then the caption, then the file's path from the site root
[TOC]labels.toc in capitals, then the numbered section titles
entryits title, the first tag right-aligned on the same line, [ tag ]; the meta line below it, a date in words only; its body indented four more spaces

Inline: a link keeps its label, and an external one adds its address in parentheses, archive (https://example.org/); an internal address is left out, since a terminal visits it with curl rather than copying it. Bold and italic are plain text; struck text keeps its ~~ marks, since dropping them would change the sense. The full dialect, block by block, is in the Markdown reference.

Code frames

A code block is framed by two rules that say which end is which: the top one opens with ., carries the language and closes with .; the bottom one opens and closes with '. The code is indented inside, with nothing added to its own lines, so it copies clean from a terminal. Shortened:

.-- sh ------------------------------.
  curl example.org/about
'------------------------------------'

The code keeps its spacing exactly; a tab moves to the next tab stop, every four columns. A line past the 75th column is cut and continued on the next line, indented two spaces, the cut marked with \: a shell reads it as a continuation.

Colours

ansi/ is the same text with ANSI escape sequences: eight colours, or a 256-colour accent the site chose (text.accent), never a background, so it reads on light and dark terminals alike. Colour follows the markup, never a guess from the words: the build marks inline code, list markers, code-block lines and, in a highlighted code block, each token, where it renders them, and colours exactly those, across line breaks.

What Colour
the header and footer rulesdim
a section's title, when it holds only letters, digits, spaces and -()'bold
the page's name, on the first line of the first sectionbold
inline codethe accent, as far as its backquotes go
list markersthe accent
URLsthe accent, underlined
[ tags ]bold, in the accent
a code block's rulesdim
a command line in a code block with no language, or in textthe accent
an information callout's box and labelthe accent, the label bold
a warning calloutyellow
an error calloutred

The accent is text.accent: "cyan" by default, or any of the eight colour names or a 256-colour index from 16 to 255, such as 208 for orange, set in site.toml (a site.<lang>.toml may set its own) (configuration). A URL's or a command line's closing punctuation - a final .,;:!?, a quote left unpaired, a bracket never opened - is left uncoloured: in a sentence such as (see https://example.org/a_(b)), only the address is coloured, not the ). that ends it.

A command line is a code-block line that starts with one of the words of text.commands, alone or after a prompt, $ or # and a space; by default only curl. A site adds its own:

[text]
commands = ["curl", "docker", "python3"]

Callouts are recognised by the label in their top rule, so the colours follow labels.info, labels.warning and labels.error in every language. Remove the escape sequences from a file of ansi/, and what is left is its twin in txt/, byte for byte. txt/ has no escape sequence at all: it survives curl > file.

Highlighted code blocks

A code block in a language highlight.py knows is highlighted in ansi/ too, each token in its kind's colour, by the same rules as the HTML page: ansi/ never disagrees with what a browser shows. Colour only, never a character added: the spaces around a token are left uncoloured, and stripped of its escapes, the block is the one of txt/. A line cut at the 75th column keeps its colours on the next line.

Token Colour
keyword; in sh and console, the command word; in markdown, [!INFO] and the like, [TOC]bold, in the accent
builtin or type; in sh and console, an option (-s, --out); in markdown, a list marker, a task box, emphasisthe accent
string, a diff + line; in markdown, inline code, a link's titlegreen
comment, a console prompt ($ , # ); in markdown, a fence line, a rule, a table's pipes, the > of an inset, the front matter's ---dim
number, variable; in markdown, a link's target, the {markers} ending a linemagenta
tag, key, section, a diff hunk header (@@); in markdown, a heading, a front matter keybold
a diff - linered

The command word is the first word of a command: at the start of a line (after a $ prompt in console), after |, ||, &&, ;, &, (, $(, after a prefix such as sudo or env, and after a variable assignment.

The languages highlighted are sh, python, js, c, go, rust, sql, json, jsonc, yaml, kyaml, ini, conf, dockerfile, html, css and make, each by its own token rules, plus console, diff and markdown, coloured line by line; the full list of names and their aliases is in code blocks. A block with no language, in text, or in a language tilder does not know is not highlighted: only its command lines (text.commands) are in the accent, and txt/ stays plain either way.

Leaving a page out

text: no in a page's front matter writes no text for it, in txt/ or in ansi/. The starter's 404 page does it: a terminal gets a short message from the server instead (writing pages, deployment).

---
title: 404
description: Page not found.
text: no
robots: noindex
---

See also

↑ back to top