tiny read-only web viewer for local source trees
clone: git clone http://polydawn.io/vitrine
Go 93.6%CSS 6.4%
vitrine๐
A tiny read-only webserver for putting local source trees on display.
Point it at some directories via a small config file, and it serves them:
markdown renders to HTML, directories become listings,
and ?fmt=raw on any URL serves the file bytes directly.
No javascript required, no uploads, no auth, no dynamic anything beyond rendering.
(URL prefixes can also forward to other local services; see Forwarding below.)
Config๐
vitrine takes exactly one config file, via the required -config flag
(there is no default location and no search).
The format is YAML:
exhibits:
moonshot-1:
path: "%"
meta/research/some-topic:
path: ../elsewhere/some-topic
some-repo:
path: ../src/some-repo
expose-git: true
exhibitsmaps URL prefixes to directories. A prefix may contain slashes; the intermediate URL levels then serve synthetic listings that reveal only the mounted subtrees.path(required) is the directory to serve. The placeholder"%"means "same as the exhibit name" -- the common case where the URL space mirrors the directory layout. Relative paths resolve against the config file's directory, which makes a config portable alongside the trees it describes. Absolute paths also work. Trailing slashes on names and paths are accepted and normalized away.expose-git(optional, default false): see below.raw(optional, default false): see below.
A bad config (parse error, unknown key, missing path, or nothing configured at all) is a startup error: vitrine refuses to run rather than serve the wrong thing.
An exhibit whose directory is absent is not a bad config, though: that exhibit is skipped with a warning and the rest serve normally, so one config can describe trees that only some hosts happen to have.
Config changes apply live: vitrine watches the config file (inotify for immediate pickup, plus a 5-second stat poll as backstop, so a change lands within ~5s worst case, usually instantly). Edit freely; a typo mid-edit logs a rejection and leaves the running server untouched.
Forwarding๐
Besides serving file trees, vitrine can reverse-proxy URL prefixes to other services, so one publicly routable port can carry both the docs and, say, a game server:
exhibits:
some-repo:
path: "%"
forwards:
/:
to: http://localhost:8866
tools/dashboard:
to: http://localhost:9090
forwardsmaps URL prefixes to backend base URLs, sibling toexhibits. Names share the URL namespace with exhibits (a collision is a config error) and appear in listings like any other mount.- The name
/is the root forward: it catches every request that no exhibit, forward, or listing claims. Exhibits keep winning at their own prefixes -- dispatch is longest-prefix-match -- so a backend at the root simply never sees URLs that collide with an exhibit prefix, its ancestors, or the reserved/-/prefix. tomust be an absolutehttp://orhttps://URL; the scheme is mandatory. Websocket backends are alsohttp://: a websocket connection begins as an ordinary http request with anUpgradeheader, which the proxy passes through, so websockets and SSE streams work with no extra configuration.- A path on the
toURL becomes a prefix on every proxied request. - The mount prefix is stripped before proxying:
a backend behind
tools/dashboardsees/x, not/tools/dashboard/x. It should therefore use relative URLs in its pages (or be told its public prefix by its own means; vitrine does not rewrite response bodies). - An unreachable backend answers 502 per-request. Backend liveness is deliberately not part of config validation: backends come and go while vitrine runs.
Forwarding is one of two deliberate exceptions to "read-only, no dynamic anything"
(the other is expose-git); vitrine adds no auth, no TLS, and no rewriting --
the forward is a pane of glass to another local port.
Theming๐
Each host wears its own accent color, derived from its hostname,
so when several vitrines from several hosts are open in tabs,
each is recognizable at a glance.
The hostname's first dot-segment (e.g. langoustine from langoustine.exe.xyz)
hashes to a hue; that name is also shown next to the site name in the nav,
so the color never has to carry the meaning alone.
The derivation is deterministic: the same hostname always yields the same color,
on every build and every host.
The -theme-seed flag substitutes any string for the hostname segment --
useful for testing, or if you dislike the color your hostname landed on.
Pages follow the browser's prefers-color-scheme, light or dark,
with no in-page toggle (vitrine ships no JavaScript, so there is nowhere to store one).
Both palettes are picked to clear WCAG AA contrast (4.5:1) against their background,
and server-rendered diagrams (see Diagrams below) invert to match.
Callouts๐
Markdown can carry callout boxes (notes, warnings, tips, and the like) using colon fences:
:::warning Optional title text
body content, which is **full markdown**
:::
- A line of three-or-more colons followed by a non-empty remainder opens a callout. The first word of the remainder is the callout type; anything after it is an optional title.
- A line of three-or-more colons alone closes it.
- The body is ordinary markdown: lists, emphasis, links, and code all work inside.
The rendered box is <div class="callout callout-{type}">
(matching the markdown-it-container / VuePress convention,
so docs authored for those tools render as intended).
The styled types are info, note, tip, success, todo, warning, danger, and reject,
each with its own accent color and icon; info and note ride the per-host accent.
warn is accepted as an alias for warning,
and the GitHub alert types caution and important map onto warning and note.
An unknown type still renders as a plain neutral box.
Callouts do not nest, by design, and an unterminated callout simply runs to the end of the document rather than breaking it.
Diagrams๐
A fenced code block tagged dot (or its alias graphviz)
renders server-side to inline SVG, so diagrams need no JavaScript in the page.
Write the DOT source in a normal fenced block:
```dot
digraph { rankdir=LR; a -> b -> c; b [shape=box]; }
```
- The source language is Graphviz's DOT.
Rendering runs entirely in-process via an embedded Graphviz WebAssembly build,
so there is no external
dotbinary to install and no browser involved. - A diagram whose DOT fails to compile does not break the page: it falls back to showing its own source in a code block, flagged with the parse error, so the author sees exactly what did not render.
- Any other code language (or none) renders as an ordinary code block, unchanged.
raw๐
By default, a file served without ?fmt=raw gets the rendered view:
markdown becomes HTML, and anything else is framed in an iframe.
Setting raw: true on an exhibit drops that frame entirely:
every file serves as its raw bytes, as if ?fmt=raw were always present
(markdown included -- it serves as text/plain like any other source file).
This turns an exhibit into a plain static file server,
which is what you want for a tree of assets meant to be consumed directly
(by tools, by <img>/<link> references, or by download)
rather than read through vitrine's chrome.
Directories are unaffected: they still render the normal listing.
expose-git๐
By default, .git directories are invisible: hidden from listings and unreachable by direct URL.
Setting expose-git: true on an exhibit flips that:
.git lists and serves like any other directory,
which makes git clone http://host/exhibit/.git work over git's dumb HTTP transport.
Two caveats, read them before enabling:
- vitrine writes into that exhibit's
.git. Dumb-HTTP clone requires.git/info/refsand.git/objects/info/packs, and ordinary commits never maintain them, so vitrine runsgit update-server-infoon the exhibit at startup and at every successful config reload. This is a deliberate exception to vitrine's otherwise strictly read-only stance. - Dumb HTTP has dumb semantics.
Clones reflect the refs as of the last
update-server-inforun, so commits made after the last startup or config reload are invisible to clones until the next one (touching the config file suffices to trigger a reload). A stale clone succeeds silently at the old tip; there is no warning. Concurrentgit gcin the exhibit can also break an in-flight clone (loudly; retry works).
Deployment๐
Run vitrine as your own user, under a systemd user unit. No root anywhere.
-
Build and install the binary somewhere on your user's path:
go build -o ~/bin/vitrine . -
Write your config, e.g.
~/fusion/.vitrine.cfg, with paths relative to that directory (see Config above). -
Install the unit file from this repo as a user unit:
mkdir -p ~/.config/systemd/user cp vitrine.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now vitrineAdjust
ExecStartin the unit if your binary or config live elsewhere. -
Keep it running after logout (user units otherwise stop with your session):
loginctl enable-linger $USER
Check on it with systemctl --user status vitrine
and journalctl --user -u vitrine -f.
README.md ยท 9.3 KiB ยท display-extract = 8027399b52
branches: display-extract, main