The lttr CLI
cmd/lttr previews, renders and checks the nine reference templates in examples/quiet. Run it with go run ./cmd/lttr <command> from the repo root, or go install github.com/sulv-io/lttr/cmd/lttr@latest for a standalone binary.
$ go run ./cmd/lttr
usage: lttr <command> [flags]
commands:
preview render every example to a directory with an index page
render write one example's .eml to stdout
lint compile template files and print errors and warnings
tags list a template's merge tags and bindings
serve live preview that recompiles templates on every request
Run "lttr <command> -h" for a command's flags.
Every command's flags come straight from its own -h; the text below is pasted from a real run, not retyped.
Preview every example
$ go run ./cmd/lttr preview -h
usage: lttr preview [-out dir] [-assets url]
Renders every example with its sample data and writes {NN}-{name}.html,
.txt and .eml, its template source as {NN}-{name}.md.txt, the logos under
assets/, manifest.json (the examples' metadata, for the docs site), and
index.html: a page to review them side by side at 600px and 375px, in light
and dark.
flags:
-assets url
logo base url; relative URLs are allowed (default "assets")
-out dir
output dir (default "preview")
go run ./cmd/lttr preview -out preview
writes, per example ({NN}-{name} such as 01-order.out_for_delivery):
{NN}-{name}.html— the HTML part{NN}-{name}.txt— the plain-text part{NN}-{name}.eml— the full MIME message (Message.MIME, dated 2026-09-24T12:00:00Z so output is stable apart from the Message-ID and boundary){NN}-{name}.md.txt— the template's own Comark source, byte for byte
plus assets/ (the embedded logos), manifest.json and index.html. manifest.json is a JSON array, one object per example, meant for tools like the docs site's own email gallery to read without invoking Go:
[
{
"number": 1,
"name": "order.out_for_delivery",
"base": "01-order.out_for_delivery",
"file": "01-order-shipped.md",
"family": "buyer",
"stream": "transactional",
"subject": "Your order is on its way",
"preheader": "Arriving at Frigate Bay by 5:30 pm today.",
"from": "CaribHubs <orders@caribhubs.com>",
"modules": ["statement", "progress", "receipt", "meta"],
"warnings": []
}
]
modules and warnings are always arrays ([], never null, even when empty). index.html is the page described in Live preview.
make preview runs this with the Makefile's defaults (go run ./cmd/lttr preview -out preview).
Render one example
$ go run ./cmd/lttr render -h
usage: lttr render -t name [-to addr]
Renders the example's sample data and writes the .eml to stdout.
flags:
-t name
template name, e.g. auth.reset
-to address
recipient address to use instead of the sample's
go run ./cmd/lttr render -t order.out_for_delivery > order.eml
go run ./cmd/lttr render -t order.out_for_delivery -to alex@example.com > order.eml
-t is required; -to must parse as an RFC 5322 address (net/mail.ParseAddress) and replaces the rendered message's To — the sample data's own recipient is used otherwise. The .eml is written straight to stdout, so redirect it or pipe it (| less, | msmtp -t, …).
Lint template files
$ go run ./cmd/lttr lint -h
usage: lttr lint [files…]
Compiles each Comark file against the data type of the example its
"template:" frontmatter names. Prints every error as file:line:col: message
and every warning as file:line:col: warning: message, and exits 1 if any
file has an error. With no files it lints the nine embedded examples.
go run ./cmd/lttr lint # the nine embedded examples
go run ./cmd/lttr lint examples/quiet/templates/*.md # your own copies on disk
With files given, each one's template: frontmatter key picks which of the nine registered data types to compile it against (there is no way to lint an arbitrary new data type from the CLI — that needs a Go test, see Goldens and testing). A file with no template: key at all is reported at a hardcoded 1:1 (frontmatter: missing "template", since there is no key to point at); a template: value that isn't a string, or an unrecognised template name, is reported at that key's actual position instead.
List merge tags and bindings
$ go run ./cmd/lttr tags -h
usage: lttr tags -t name
Lists the merge tags and bindings of a template, one per line, sorted by
position: path<TAB>go type<TAB>binding|tag<TAB>line:col
flags:
-t name
template name, e.g. order.out_for_delivery
$ go run ./cmd/lttr tags -t order.out_for_delivery
destination string tag 5:25
eta string tag 5:44
order_id string tag 10:28
driver.first_name string tag 13:1
picked_up_at string tag 13:47
destination string tag 13:79
eta string tag 13:98
track_url string tag 15:31
order data.Receipt binding 21:1
deliver_to.name string tag 26:1
…
Each line is path, the Go type it resolves to, tag or binding (a {{path}} merge tag versus a :key="path" binding), and its line:col in the source — the same shape TagInfo exposes as a Go value (see Merge tags and bindings).
Serve a live preview
$ go run ./cmd/lttr serve -h
usage: lttr serve [-addr host:port] [-dir dir]
Serves a live preview of the examples. Every request compiles the template
sources in -dir from disk, so edits show on the next load; the index page
reloads its previews when a file changes. Compile and render errors are shown
as file:line:col: message.
flags:
-addr address
listen address (default "localhost:7070")
-dir dir
template source dir (default "examples/quiet/templates")
See Live preview for what it serves and how reload works. make serve runs it with the Makefile's defaults.
Read the exit codes
Every command returns:
| Code | Meaning |
|---|---|
0 | Success — including -h, -help, --help or help, at the top level or on a subcommand |
1 | The command ran but failed — an unknown template name, an invalid -to address, a compile or render error, a file that can't be read, a listener that can't bind |
2 | A usage error — bare lttr with no arguments, an unrecognised command, a missing required flag (-t), an unparseable flag, or too many positional arguments |
lttr preview -h prints its usage and exits 0; lttr render -bogus (a flag the command doesn't recognise) prints the flag error and usage and exits 2; lttr with no arguments at all also prints the top-level usage but exits 2, not 0 — only an explicit -h/-help/--help/help gets the success exit code.
Next steps
- Live preview for
serveandpreviewin more depth. - Screenshots for
make shoot, built on top ofpreview.