Tooling

Live preview

lttr serve's reload loop, its error display, the index page's controls, and how make preview differs.

lttr serve (D17) is the day-to-day way to see templates while editing them: it compiles from disk on every request, so a save shows up on the next load with no restart.

Terminal
make serve
# or directly:
go run ./cmd/lttr serve -addr localhost:7070 -dir examples/quiet/templates

opens http://localhost:7070.

Watch the reload loop

Unlike lttr preview, which renders the nine embedded sources once into static files, serve reads each template's file from -dir (default examples/quiet/templates) fresh on every request — so editing examples/quiet/templates/01-order-shipped.md and reloading shows the change immediately, no rebuild.

The index page itself polls GET /_version once a second. /_version hashes the name, size and modification time of every file in -dir; when the hash changes, the page:

  1. Fetches / again and swaps in the refreshed sidebar list and detail sections (innerHTML, not a full page reload).
  2. Re-selects whatever template was open and reloads its preview frames from /t/{base}.html?v={version} — the version query string busts the browser's cache for that frame.

The live badge in the header shows this is happening (green dot, "live"); if /_version fails to answer, it flips to "offline" (grey) until polling succeeds again. This loop is what makes lttr serve a live preview and lttr preview's output a one-shot snapshot.

Read errors in the preview

A compile or render error never breaks the page. Errors are formatted as file:line:col: message (or file: message for one with no position) and shown two places:

  • The sidebar replaces that template's family/stream/subject line with a red N error(s) badge.
  • Its preview frame (/t/{base}.html) serves a small standalone error page — dark red on a light red background, monospace, one <li> per error line — in place of the rendered HTML, with HTTP status 200 (not 500), so it renders inside the iframe instead of showing the browser's own error page.

/t/{base}.txt and /t/{base}.eml return the same error lines as plain text when the template fails, rather than a broken text part or a .eml that can't parse.

Use the index controls

The same index.html (embedded via //go:embed index.tmpl, shared by preview and serve) has a header with three control groups plus a standalone page-theme button, and keyboard shortcuts:

ControlValuesKeyboardWhat it does
Email colour schemeLight / DarkdSets color-scheme on each preview <iframe> and, when the frame's document is readable, flips the email's own prefers-color-scheme media rules directly through the CSSOM — some browsers don't propagate the iframe's color-scheme into the frame, so this is the belt-and-braces fix (D18, amended by D32).
Preview widthBoth / 600 / 375wShows one or both size frames (the templates are built for 600px desktop and 375px mobile).
PartHTML / Plain texttToggles between the rendered HTML frames and the plain-text body shown as preformatted text.
Page theme(button, no group)—Switches the page's own chrome between light and dark — independent of the email colour scheme above, and unrelated to it.

j/k (or the arrow keys) move to the next/previous template in the sidebar. Every choice is remembered in localStorage and restored on the next visit; the page theme choice is applied before first paint via an inline script, so the chrome never flashes light-then-dark.

The static lttr preview output embeds each rendered email into its frame with srcdoc (rather than an iframe src URL) precisely so the page still works when opened directly as a file:// document, with no server behind it (D32); lttr serve instead points frames at /t/{base}.html, since it has a server to load from and wants each reload to hit it.

Run make preview

Terminal
make preview

runs go run ./cmd/lttr preview -out preview, the static, one-shot equivalent — see The lttr CLI for exactly what it writes (.html, .txt, .eml, .md.txt, assets/, manifest.json, index.html). Open preview/index.html directly in a browser; there's no server and no reload loop, so it's the form to hand someone for a one-off review, or to feed to make shoot for the screenshot pass.

Next steps

Copyright © 2026