Tooling

The lttr CLI

Every lttr subcommand, its flags straight from -h, and its exit codes.

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.

Terminal
$ 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

Terminal
$ 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")
Terminal
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:

manifest.json
[
  {
    "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

Terminal
$ 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
Terminal
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

Terminal
$ 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.
Terminal
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

Terminal
$ 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
Terminal
$ 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

Terminal
$ 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:

CodeMeaning
0Success — including -h, -help, --help or help, at the top level or on a subcommand
1The 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
2A 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

Copyright © 2026