Introduction
lttr is a Go library for email templates. Authors write each email in Comark: Markdown plus components such as ::statement, ::receipt and :button. You give the template a Go data type. lttr.Compile checks the template against that type once, at startup, and Render fills in the data and returns a *lttr.Message with Outlook-safe HTML, a plain-text part and an envelope, ready to send as MIME.
The library is brand-neutral. It holds no colours, fonts, logo, senders or footer copy of its own; you supply them as a Theme and a set of families. The only English it writes is a handful of generic defaults, such as the sign-off "Thanks," and the tile link "Read".
Follow a template through lttr
var orderShipped = lttr.MustCompile[OrderShipped](families, "order.shipped", src)
msg, err := orderShipped.Render(renderer, recipient, OrderShipped{OrderID: "A-1042"})
Three things meet in these two lines:
- The template is Markdown with YAML frontmatter. The frontmatter names the template and its family and holds the subject and preheader. The body is a stack of modules, and loose Markdown becomes a letter module. Merge tags (
{{order_id}}) and bindings (:order="order") refer to fields of your type. - The family is a kind of email you send, such as order updates or password resets. It fixes the sender, the sending stream, the header and footer, the footer copy, the modules its templates may not use and whether opens and clicks may be tracked. You collect families in a
Registry, andCompileapplies their rules. - The stream is how the family's mail is sent:
Transactional,Security,Marketing,OutreachorInternal. ARoutersends each stream through its ownSender, so marketing mail and password resets can go out through different senders and keep separate reputations.
Compile does everything that does not depend on data, and Render does the rest. A misspelt merge tag, an unknown module attribute or a module the family forbids is a compile error. A merge tag with no value and no fallback, an invalid URL or an empty subject is a render error. Render never returns a partial message.
See the pipeline
Each stage is its own package, and each one builds on the stages before it:
| Stage | Package | When | What it does |
|---|---|---|---|
| Parse | comark | Compile | Reads the Markdown, components, attributes and merge tags into an AST with line and column positions. |
| Check | ir | Compile | Turns the AST into typed modules, applies the module rules, and resolves every merge tag and binding against your type. |
| Arrange | layout | Render | Adds the family's header and footer and sets the spacing between modules. |
| Draw | render/html, render/text | Render | Writes the table-based HTML and the plain-text part from the same arranged document. |
| Package | lttr | Render, MIME | Builds the Message with its envelope and headers, and serialises it as multipart/alternative. |
theme (palettes, fonts, the brand mark) and data (Money, Receipt and the other typed values) sit beside the pipeline, and every stage may use them.
Know what lttr does not do
- No provider SDKs. lttr sends over SMTP, or gives you the raw MIME bytes with
Message.MIME. SES, Postmark or anything else is aSenderyou write, or an SMTP relay you point at. - No web fonts. Your theme names three CSS font stacks (serif, sans, mono). lttr writes no
@font-facerule or font link, so the stacks should name fonts the recipient already has. - No visual editor. Templates are text files in your repository, reviewed and tested like code.
- No layout control in templates. Templates choose modules. The layout decides the spacing, the family decides the header and footer, and colours come from the theme, so a template never sets a margin.
Learn where it came from
lttr was built at SULV for CaribHubs, its first consumer; the Quiet example is the design it was built to reproduce.