rmd
Read a markdown file with browser-grade typography - one command, one
self-contained page, no process left running.

rmd <file.md> renders a markdown file into a single self-contained HTML page,
serves it once on a random loopback port, opens it in your default browser, and
exits as soon as the page confirms it loaded. Every asset - fonts, CSS, JS,
local images, Mermaid, KaTeX - is inlined into that one HTML document, so the
tab keeps working (themes, font sizes, diagrams) long after the process is gone.
rmd notes.md # render, open in the browser, exit
rmd -version # print version
Quick install via the personal Homebrew tap (other methods under
Install):
brew install dsbasko/tap/rmd # install
brew upgrade rmd # update to the latest release
Contents
Why
Reading .md in a terminal or an IDE is a compromise: monospaced everything, no
real typography, no themes. The usual fixes are worse in their own way - a
dedicated viewer app you have to install and keep updated, or a grip-style
preview server that stays resident for as long as the tab is open.
rmd is the third option: browser typography with a one-shot process. It does
its job - render, serve once, open - and exits. The page it leaves behind is
fully autonomous: close the laptop, go offline, reopen the tab tomorrow - the
theme switcher and every diagram still work, because nothing on that page
references the network or the dead server.
How it works
- Parse. goldmark with GFM, footnotes,
and auto heading ids turns the markdown into HTML. Along the way, AST
transformers inline local
![]() images as data:<mime>;base64,…, convert
```mermaid fences into <pre class="mermaid"> blocks, and capture
math ($$…$$, \(…\), \[…\]) at parse time, before CommonMark can mangle
the LaTeX. Code blocks get chroma
classes with a palette generated per theme.
- Assemble.
html/template + go:embed build one self-contained page:
CSS, JS, woff2 fonts, mermaid.min.js, and katex.min.js are embedded into
the binary and inlined into the HTML. Mermaid and KaTeX are included only
when the document actually uses them. There is not a single external URL in
the output.
- Serve and open. An HTTP server starts on
127.0.0.1:<random port>, the
URL is printed to stdout, and the browser opens it (open / xdg-open /
cmd depending on the platform).
- Exit. On
window.load the page sends navigator.sendBeacon('/done');
the server shuts down and the process exits 0.
Two safety valves keep it from hanging around:
- no
GET / within 60 s → exit 1 (the URL is already on stdout, so you can
still open it by hand);
- page served but no beacon within 15 s → exit
0 (the content was delivered
in full; the server has nothing left to do).
Install
Homebrew (easiest):
brew install dsbasko/tap/rmd # install
brew upgrade rmd # update to the latest release
Installs from the personal tap
dsbasko/homebrew-tap (not
homebrew-core), building from source. The build needs no network access beyond
fetching the tarball: all page assets are committed to the repository and
embedded via go:embed.
Quick (go install):
go install github.com/dsbasko/rmd/cmd/rmd@latest
Drops the binary in $(go env GOPATH)/bin.
From a clone:
git clone https://github.com/dsbasko/rmd
cd rmd
make install # go install into $GOBIN
make build # or: local binary in ./bin/rmd
Usage
rmd notes.md # render and open in the browser
rmd -version # print version and exit
What happens on a run:
- the markdown is parsed into a self-contained HTML page;
- a local server starts on
127.0.0.1:<random>, its URL is printed to stdout;
- the page opens in the default browser (a failed open is a warning, not
fatal - the URL is already printed);
- after
window.load the browser sends navigator.sendBeacon('/done'), the
server shuts down, the process exits 0.
The tab stays fully functional after the process ends - theme and size
switching work offline.
Themes and sizes
Settings are not persisted: every run starts from the system theme
(prefers-color-scheme) and size m. The toolbar in the top-right corner
controls both; choices live until the tab is closed (no localStorage).
- Themes: ☀ light · ☾ dark · ¶ paper. With no choice made, the system
light/dark preference applies; paper is manual-only.
- Sizes: xs / s / m / l / xl →
--prose-font-size 13 / 14.5 / 16 / 18 /
20 px (code font ~2 px smaller), with the content column width scaling along.
Default is m.
Rendering features
- GFM: tables, blockquotes (including nested), task-list checkboxes,
strikethrough, autolinks, footnotes.
- Code highlighting: chroma with CSS classes; palettes generated per theme
(github for light, github-dark for dark, solarized-light for paper).
- Mermaid:
```mermaid fences render client-side; diagram theme
follows the page theme.
- KaTeX:
$$…$$, \(…\), \[…\]. Single $…$ is deliberately not
supported, so "from $10 to $20" never turns into math.
- Images: local paths (relative or absolute) are inlined as
data:<mime>;base64,…; http(s):// and data: sources are left as is; a
missing file produces a warning on stderr and the node is left untouched.
- Fonts: Roboto Slab (prose) and JetBrains Mono (code) are embedded with
Latin and Cyrillic subsets - nothing needs to be installed in the OS.
Exit codes
| Code |
Meaning |
0 |
Clean exit: beacon received, beacon timeout after serving, or Ctrl+C. |
1 |
Error: file missing / not a regular file, render failure, or no GET within 60 s. |
2 |
Usage: no positional argument or an unknown flag. |
Known limitations
- Raw HTML
<img src="…"> and url() inside user HTML are not inlined -
only markdown ![]() nodes are processed.
- GitHub alerts, TOC generation, and multi-file navigation are not supported -
one file per run.
- No theme/size CLI flags - the on-page toolbar is the only control.
Building from source
Go 1.26+, no cgo, no network needed for the build - every embedded asset is
committed to the repository.
make build # ./bin/rmd with -ldflags "-s -w -X main.version=…"
make test # go test ./...
make lint # golangci-lint run
make install # go install into $GOBIN
make clean # remove ./bin
Rough layout:
cmd/rmd/ CLI entry point; run(args, stdout, stderr) int is the testable wrapper
internal/render/ goldmark → Result{HTML, Title, HasMermaid, HasKatex, Warnings}
internal/page/ Build(Params) []byte - html/template + go:embed → self-contained HTML
internal/server/ one-shot loopback server: Start() → url; Wait(ctx) handles shutdown
internal/browser/ Open(url) - open / xdg-open / cmd behind a mockable platform runner
scripts/ vendor-assets.sh - re-vendors the pinned embedded assets
Updating embedded assets (fonts, Mermaid, KaTeX) goes through
scripts/vendor-assets.sh - versions are pinned in its header, and the result
is committed to the repository.
License
Released under the MIT License. See LICENSE for the full text.
© 2026 Dmitriy Basenko.