Reference

API reference

Every exported identifier of lttr and its sub-packages, package by package, with its signature and what it does.

A map of the public surface, generated from go doc -all and grouped by package. Each section links to the page that explains it in context. Most services import only the root package, plus data for the typed values and theme for the palette types. The other packages are the stages of the pipeline (comark → ir → layout → render/html and render/text). They are exported so tools and tests can use them.

internal/htmlcheck, the structural lint for rendered HTML used by the goldens, is internal and can't be imported from outside the module.

Import lttr

import "github.com/sulv-io/lttr"

The API: compile templates, render messages, and send them.

Compile templates

func Compile[T any](reg *Registry, name string, src []byte) (*Template[T], error)
func MustCompile[T any](reg *Registry, name string, src []byte) *Template[T]

type Template[T any] struct{ /* unexported */ }

func (t *Template[T]) Render(r *Renderer, to Recipient, v T) (*Message, error)
func (t *Template[T]) Meta() TemplateMeta
func (t *Template[T]) Name() string
IdentifierDoes
CompileCompiles src called name against the data type T (which must be a struct) and the families in reg. It checks the Comark source, frontmatter, modules and props, every merge tag and binding against T and Recipient, and the family rules. It returns every error joined, each an *ir.Error with name:line:col.
MustCompileCompile that panics on error, for package-level template variables.
Template[T]A compiled template, immutable and safe for concurrent use.
Template.RenderRenders for recipient to with data v and runs the data-dependent checks (values, URLs, subject, addresses, entity, unsubscribe URL, the 100 KB size limit). It returns a whole *Message or an error, never both.
Template.MetaA copy of the template's TemplateMeta.
Template.NameThe template name.
type TemplateMeta struct {
    Name     string
    Family   string
    Stream   Stream    // the family's stream, or its legal stream with "legal: true"
    Tags     []TagInfo // in source order
    Modules  []string  // module kinds in order, e.g. "statement"
    Warnings []string  // "name:line:col: warning: msg"
}

type TagInfo struct {
    Path    string // dotted path, e.g. "driver.first_name"
    Type    string // the Go type it resolves to, e.g. "string", "data.Money"
    Pos     string // "line:col" in the template source
    Binding bool   // a :key="path" binding rather than a {{path}} tag
}

TemplateMeta describes a compiled template. TagInfo is one merge tag or binding it uses, and is what lttr tags prints. See Compile errors and Merge tags and bindings.

Configure a renderer

func NewRenderer(cfg Config) (*Renderer, error)

type Renderer struct{ /* unexported */ }

type Config struct {
    Domain    string // sending domain; the {domain} placeholder
    SiteURL   string // absolute https URL; the {site} placeholder
    AssetBase string // base URL of the logo files

    Theme    Theme     // palettes, fonts and brand mark
    Families *Registry // the family registry templates are compiled against

    Entities      map[string]Entity // region → entity, for the footer address
    DefaultRegion string            // region used when a recipient has none; must be in Entities

    AllowRelativeURLs bool // previews only: relative asset and link URLs are allowed
}

type Entity struct {
    Address string
}
IdentifierDoes
NewRendererValidates cfg, including the theme and the registry, and compiles every family's copy. It reports every problem at once.
RendererRenders compiled templates for one Config. Immutable and safe for concurrent use.
ConfigEverything a Renderer needs from the consumer.
EntityThe legal entity a region's mail is sent on behalf of. Address fills {address}.
type Vars struct {
    Site        string `json:"site"`        // Config.SiteURL
    Brand       string `json:"brand"`       // Theme.Brand.Name
    Domain      string `json:"domain"`      // Config.Domain
    Region      string `json:"region"`      // the recipient's region, or Config.DefaultRegion
    Address     string `json:"address"`     // the entity address for the region
    About       string `json:"about"`       // the F2 about value (family copy or footer.about)
    Prefs       string `json:"prefs"`       // the recipient's preferences URL
    Unsubscribe string `json:"unsubscribe"` // the recipient's unsubscribe URL
    Issue       string `json:"issue"`       // the evaluated frontmatter issue (H3 masthead)
}

Vars are the {name} placeholders available to family copy and the footer.* frontmatter overrides. The renderer fills them per message.

type Theme = theme.Theme
type Recipient = data.Recipient

Theme is the brand a renderer draws with (see theme). Recipient is the person a message is addressed to (see data). Templates can use its fields as merge tags. See Config and the renderer.

Define families

func NewRegistry(families ...Family) *Registry

type Registry struct{ /* unexported */ }

func (r *Registry) Lookup(name string) (Family, bool)
func (r *Registry) Names() []string
func (r *Registry) Validate(domain string) error
IdentifierDoes
NewRegistryA registry of deep copies of families. It never fails, because problems are reported by Validate.
RegistryAn immutable set of families, handed to Compile and to Config.Families.
Registry.LookupA copy of the family called name. With duplicate names, the first wins.
Registry.NamesThe family names, sorted, each once.
Registry.ValidateReports every problem for a config whose Domain is domain: duplicate or empty names, invalid streams or variants, a missing masthead (H3) or banner (H2), unknown forbidden modules, a sender outside its domain, and copy that doesn't compile. NewRenderer calls it.
type Family struct {
    Name            string
    From            mail.Address   // fixed sender; zero when FromFrontmatter
    FromFrontmatter bool           // the template's frontmatter "from" is required and is the sender
    SenderDomain    string         // allowed sender domain; "" = Config.Domain; may use {domain}
    Stream          Stream         // sending stream
    Header          HeaderVariant  // header chrome
    Footer          FooterVariant  // footer chrome
    Copy            FooterCopy     // footer copy for Footer
    Masthead        string         // H3 masthead copy, e.g. "Journal · No. {issue}"; required for H3
    Banner          string         // H2 banner copy; required for H2
    Legal           *LegalOverride // what frontmatter "legal: true" switches to; nil = not allowed
    Forbidden       []ir.Kind      // modules templates of this family may not use
    Tracking        bool           // whether the sending service may track opens and clicks
}

type FooterCopy struct {
    Nav       []NavLink  // F1 nav row; empty = no row
    Reason    string     // F1 lead (Markdown)
    Links     string     // F1 links after the reason
    Note      string     // F2 lead (Markdown)
    About     string     // F2 {about} value (plain text); "" drops the service line
    Service   string     // F2 service line
    OptOut    string     // F3 lead (Markdown)
    Internal  string     // F3·i lead (Markdown)
    Address   string     // every variant, plain text, e.g. "{brand} · {address}"
    TextLinks []TextLink // plain-text URL lines after the address
}

type LegalOverride struct {
    Stream Stream
    Footer FooterVariant
    Copy   FooterCopy
}

type NavLink struct{ Label, Href string }
type TextLink struct{ Label, URL string }
IdentifierDoes
FamilyA consumer-defined kind of email: sender, stream, chrome, footer copy and forbidden modules.
FooterCopyA family's footer copy. Each string is one paragraph of Markdown, or plain text, with {name} placeholders and no merge tags. The footer variant decides which fields are shown.
LegalOverrideThe stream, footer and copy a family switches to when a template sets legal: true.
NavLinkOne link of the F1 nav row. Both fields may use placeholders.
TextLinkOne plain-text footer line, Label: URL. URL may use placeholders. Label is literal.
type HeaderVariant = layout.HeaderVariant
type FooterVariant = layout.FooterVariant

const (
    H1 = layout.H1 // logo
    H2 = layout.H2 // logo with the internal banner (Family.Banner)
    H3 = layout.H3 // newsletter masthead (Family.Masthead)
)

const (
    F1         = layout.F1         // marketing: nav, reason, unsubscribe and preferences links
    F2         = layout.F2         // service: note and "service email about {about}" line
    F3         = layout.F3         // outreach: opt-out line
    F3Internal = layout.F3Internal // internal: internal-only line
)

The header and footer chrome variants. See Families and streams.

Pick a stream

type Stream string

const (
    Transactional Stream = "transactional" // mail the recipient triggered: receipts, orders, payouts
    Security      Stream = "security"      // account security: sign-in codes, password resets
    Marketing     Stream = "marketing"     // opted-in promotional mail; carries List-Unsubscribe
    Outreach      Stream = "outreach"      // business-to-business mail, kept apart from Marketing
    Internal      Stream = "internal"      // mail to the operator's own staff only
)

func (s Stream) Valid() bool

A message's sending stream. Streams keep reputations apart: a Router sends each one through its own Sender. Valid reports whether s is one of the five.

Work with a message

type Message struct {
    Template, Family   string       // not written to the wire
    Stream             Stream       // selects the Router's sender
    From, To           mail.Address // sender and the single recipient
    Subject, Preheader string
    HTML, Text         string       // the two bodies of the multipart/alternative message
    Tracking           bool         // whether the HTML carries tracked links (the family's Tracking)
    Warnings           []string     // non-fatal findings from compile and render
    // unexported header fields
}

func (m *Message) MIME(date time.Time) ([]byte, error)
func (m *Message) SetHeader(name, value string) error
func (m *Message) SetListUnsubscribe(u string) error
func (m *Message) Header(name string) string
func (m *Message) Headers() []string
func (m *Message) Clone() *Message
IdentifierDoes
MessageA rendered email: envelope, both bodies and any extra headers.
MIMESerialises an RFC 5322 multipart/alternative message with CRLF line endings: text part first, then HTML, both UTF-8 quoted-printable. Headers go in a fixed order, and non-ASCII subjects and names are RFC 2047 encoded. The Message-ID and boundary are random, so two calls differ.
SetHeaderSets a custom header. It refuses the headers MIME writes itself and List-Unsubscribe*, and refuses names or values with CR, LF, NUL or non-printable bytes.
SetListUnsubscribeSets the RFC 8058 one-click List-Unsubscribe and List-Unsubscribe-Post headers. u must be absolute https. Render calls it on the marketing stream.
HeaderA custom header's value, matched case-insensitively, or "".
HeadersThe custom header names, canonical and sorted, in the order MIME writes them.
CloneA deep copy. Changing its headers or warnings doesn't affect the original.

See Message and MIME.

Send

type Sender interface {
    Send(ctx context.Context, m *Message) error
}

type Router struct {
    Streams         map[Stream]Sender
    InternalAllowed func(addr string) bool                                     // nil denies all Internal mail
    Suppressed      func(ctx context.Context, s Stream, addr string) (bool, error) // nil suppresses nothing
}

func (r *Router) Send(ctx context.Context, m *Message) error

type SMTP struct {
    Addr    string            // host:port
    Auth    smtp.Auth         // nil sends without authentication
    Headers map[string]string // added to a copy of every message, e.g. X-SES-CONFIGURATION-SET
    Now     func() time.Time  // stamps the Date header; nil means time.Now
}

func (s *SMTP) Send(ctx context.Context, m *Message) error

var ErrSuppressed = errors.New("recipient suppressed")
IdentifierDoes
SenderDelivers a message.
RouterSends each message through the Sender for its stream, after the internal-mail and suppression checks. It never modifies the message.
Router.SendFails if the stream has no sender, if the recipient isn't a plain local@domain, if Internal mail goes to an address InternalAllowed doesn't accept, or if the recipient is suppressed.
SMTPA Sender that delivers each message over one SMTP session.
SMTP.SendChecks the message before connecting, then sends a copy with Headers added. MAIL FROM is m.From.Address and RCPT TO is m.To.Address. It uses STARTTLS when the server offers it, verified against the host of Addr. Cancelling ctx aborts the dial and the session.
ErrSuppressedReturned, wrapped with the address, when Suppressed reports the recipient. Test with errors.Is.

See Router and streams and SMTP.

Version

const Version = "v0.0.0-dev"

The library's version string as written in source. The release workflow tags commits without rewriting it, so read a consumer's lttr version from its module graph instead. See Releasing.

Import lttr/comark

import "github.com/sulv-io/lttr/comark"

The parser: Markdown with frontmatter, block and inline components, spans, element attributes and merge tags, built on goldmark. See Comark syntax.

func Parse(src []byte) (*Document, error)
func ParseAttrs(s string, base Pos) (Attrs, int, error)
func ParseText(s string, base Pos) (Text, error)
func NodeAttrs(n ast.Node) (Attrs, bool)
func PlainText(n ast.Node, src []byte) string
FunctionDoes
ParseParses a template. Safe for concurrent use and never panics. Errors are *Error values (line:col: msg). A leading BOM is ignored and CRLF is accepted.
ParseAttrsParses an attribute list at the start of s (which must start with {). It returns the attributes and the bytes consumed. {{ never starts attributes, and attributes never span lines.
ParseTextSplits s into literals and merge tags. Malformed tag attempts become literal parts with Suspect set. The only error is invalid UTF-8. \{{ is a literal {{.
NodeAttrsThe attributes of a node: those attached to a link, image, emphasis or code span, or the Attrs of a component or span.
PlainTextThe text under a node, with escapes and entities resolved and merge tags written back as {{path}}. Raw HTML is skipped.
type Document struct {
    Frontmatter    map[string]any
    FrontmatterPos map[string]Pos // dotted key path ("footer.note") → key position
    Root           ast.Node
    Source         []byte         // the source without a leading BOM
}

func (d *Document) Pos(n ast.Node) Pos

type Pos struct{ Line, Col int }
func (p Pos) String() string // "3:7"

type Error struct {
    Pos Pos
    Msg string
}
func (e *Error) Error() string // "line:col: msg"
TypeIs
DocumentA parsed template. Pos returns a node's line:col, falling back to its first positioned descendant and then its nearest ancestor.
PosA 1-based line and byte column.
ErrorA parse error at a position. Callers prefix the template name.
type Attrs struct {
    ID       string
    Classes  []string
    Values   map[string]Text   // plain attributes; a flag maps to an empty Text
    Flags    map[string]bool   // bare keys with no value
    Bindings map[string]string // :key="path"
    Pos      Pos               // position of the opening "{"
}

type Text []Part
func (t Text) String() string

type Part struct {
    Lit     string // set when Tag == nil
    Tag     *Tag
    Suspect bool   // Lit came from a malformed "{{" tag attempt
}

type Tag struct {
    Path        string // dotted, e.g. "driver.first_name"
    Fallback    string
    HasFallback bool
    Pos         Pos
}
func (t Tag) String() string // {{path}} or {{path | fallback}}

Attrs is a parsed {#id .class key=value flag :binding=path} list. Text is literal text with embedded merge tags, and Part is one piece of it. Tag is one merge tag. Its String quotes the fallback when it wouldn't survive a bare round trip.

The AST nodes all implement goldmark's ast.Node, through Kind() and Dump(src []byte, level int):

NodeSyntaxFields
Component::name{attrs} … ::Name, Attrs, Props map[string]any (nil without a props block), PropsPos, At
Slot#name inside a componentName, At
InlineComponent:name[label]{attrs}Name, Attrs, At (the label is its children)
Span[text]{attrs}Attrs, At
MergeTag{{path}}, {{path | fallback}} in proseTag
var (
    KindComponent       = ast.NewNodeKind("Component")
    KindSlot            = ast.NewNodeKind("Slot")
    KindInlineComponent = ast.NewNodeKind("InlineComponent")
    KindSpan            = ast.NewNodeKind("Span")
    KindMergeTag        = ast.NewNodeKind("MergeTag")
)

The node kinds those nodes return from Kind().

Import lttr/data

import "github.com/sulv-io/lttr/data"

Money, the structured types templates bind to, the recipient, and the path resolver the compiler uses to check merge tags. See Merge tags and bindings.

Money

type Money struct {
    Minor    int64    // minor units, e.g. cents
    Currency Currency
}

func XCD(minor int64) Money
func USD(minor int64) Money
func (m Money) String() string // "EC$1,019.90", "US$378", "−US$65"
func (m Money) Approx() string // "≈ US$378"

type Exact Money
func (e Exact) String() string // always two decimals: "EC$1,085.00"

type Currency string

const (
    XCDCode Currency = "XCD" // EC$
    USDCode Currency = "USD" // US$
    EURCode Currency = "EUR" // €
    GBPCode Currency = "GBP" // £
)
IdentifierDoes
MoneyAn amount in a currency's minor units.
XCD, USDBuild a Money in Eastern Caribbean or US dollars.
Money.StringA currency prefix, comma thousands separators, decimals dropped for whole amounts, and a U+2212 minus before the prefix.
Money.ApproxRounded half away from zero to whole units, prefixed ≈ .
ExactA Money that always shows two decimals, for breakdown copy that must match exactly.
CurrencyAn ISO 4217-style code. The four constants have a known prefix and formatting.

Structured types

type Receipt struct {
    Lines      []ReceiptLine
    Fees       []Fee
    Total      Money
    TotalUSD   *Money // optional; shown as ≈ US$
    TotalLabel string // default "Total"
}

type ReceiptLine struct {
    Name, Detail string
    Qty          int
    Amount       Money
    Thumb        string
    Tone         Tone
}

type Fee struct {
    Label  string
    Amount Money
}

type Product struct {
    Name        string
    Price       Money
    Href, Image string
    Tone        Tone
}

type StockItem struct {
    Name, SKU string
    Left      int
}

type Tone string

const (
    ToneNone Tone = ""
    ToneWarm Tone = "warm"
    ToneSea  Tone = "sea"
    ToneSun  Tone = "sun"
    ToneMap  Tone = "map"
)

func (t Tone) Valid() bool
TypeBinds to
Receipt::receipt{:order="…"}: line items, fees and a total.
ReceiptLineOne line of a receipt.
FeeA labelled charge on a receipt, such as delivery.
ProductOne tile of ::products{:items="…"} (a []Product).
StockItemOne row of ::stock{:items="…"} (a []StockItem).
ToneThe placeholder ground a tile or band shows with no image. Valid reports whether it is a known value.

Recipient

type Recipient struct {
    Address        string // RFC 5322 address
    FirstName      string `json:"first_name"`
    Region         string `json:"region"`
    EntityAddress  string `json:"entity_address"`
    PreferencesURL string `json:"preferences_url"`
    UnsubscribeURL string `json:"unsubscribe_url"`
}

The person an email is addressed to. It is always searched second when a path is resolved, after the template's own data type, so {{first_name}} works in every template. The root package re-exports it as lttr.Recipient.

Path resolution

func NewScope(roots ...Root) Scope
func (s Scope) Resolve(path string) (Accessor, error)

type Root struct {
    Slot int
    Type reflect.Type
}

func (a Accessor) Get(roots []reflect.Value) (reflect.Value, bool)
func (a Accessor) Path() string
func (a Accessor) Type() reflect.Type

func IsScalar(t reflect.Type) bool
func FormatScalar(v reflect.Value) string
IdentifierDoes
Scope, NewScopeAn ordered, immutable set of roots. A path's first segment is looked up in each root in turn, and the first match wins.
RootOne root of a scope: the value at Slot in the slice passed to Get.
Scope.ResolveCompiles a dotted path into an Accessor once, at compile time. It follows JSON names, promoted fields, pointers and map keys.
AccessorA precompiled path, a plain value that is safe to copy.
Accessor.GetWalks the path against the roots. It returns false on a nil pointer or a missing map key, and never parses or allocates path strings.
Accessor.Path, Accessor.TypeThe dotted path, and the type it resolves to with pointers dereferenced.
IsScalarWhether a type may appear in a merge tag: a value-receiver fmt.Stringer, a string kind or an integer kind.
FormatScalarA scalar's text: String(), the string itself, or the decimal integer.

Import lttr/ir

import "github.com/sulv-io/lttr/ir"

The compiler: a parsed template becomes typed module IR, with every merge tag resolved against the data type at compile time, so rendering never parses paths. Most consumers only touch ir.Error (to read compile errors) and ir.Kind (for Family.Forbidden). See Compile errors and Modules.

Compile

func Compile(name string, src []byte, opts Options) (*Document, error)
func CompileCopy(label, src string, vars reflect.Type) (Inline, error)
func CompileCopyText(label, src string, vars reflect.Type) (Text, error)

type Options struct {
    Data      reflect.Type // the template's T
    Recipient reflect.Type // reflect.TypeOf(data.Recipient{})
    Vars      reflect.Type // lttr.Vars: {name} placeholders in footer.* frontmatter
}

type Document struct {
    Name     string
    Front    Frontmatter
    Modules  []Module
    Warnings []Diagnostic
    Tags     []TagUse
}
IdentifierDoes
CompileParses and compiles src into a Document: frontmatter, implicit letters, typed modules, resolved merge tags and bindings, and every static rule. It returns the document or every error joined, never both. Family rules are checked by lttr.Compile, not here.
CompileCopyCompiles family copy: one paragraph of Markdown with {name} placeholders from vars and no merge tags.
CompileCopyTextThe same for plain copy with no Markdown.
OptionsThe Go types a template is compiled against.
DocumentA compiled template, immutable once returned.

Errors and diagnostics

type Error struct {
    Name string // template (or copy) name
    Pos  Pos
    Msg  string
}
func (e *Error) Error() string // "name:line:col: msg", or "line:col: msg" without a name

type Diagnostic struct {
    Pos Pos
    Msg string
}
func (d Diagnostic) Format(name string) string // "name:line:col: warning: msg"

type Pos = comark.Pos

Error is a compile or render error at a position. Reach it through a joined error with errors.As. Diagnostic is a compile warning, and warnings never fail compilation.

Frontmatter and tags

type Frontmatter struct {
    Template, Family                string
    Subject, Preheader, From, Issue Text
    Legal                           bool
    Footer                          FooterOverrides
    Pos                             map[string]Pos // dotted key → key position
}

type FooterOverrides struct {
    Note, Reason, OptOut *Inline // Markdown, one paragraph
    About                *Text   // F2 {about}; empty drops the service line
    Nav                  *bool   // F1 nav row
}

type TagUse struct {
    Path    string
    Type    reflect.Type // the resolved type, pointers dereferenced
    Pos     Pos
    Binding bool         // a :key="path" binding rather than a {{path}} tag
}

Frontmatter is the compiled template header. FooterOverrides holds the footer.* keys, where nil means "use the family copy". TagUse records one merge tag or binding, and lttr.TagInfo is its string form.

Modules

type Module interface {
    Kind() Kind
    Class() Class
    Pos() Pos
}

type Kind string
func Lookup(name string) (Kind, bool)

type Class uint8

const (
    ClassHero Class = iota + 1
    ClassBand
    ClassTabular
    ClassBody
    ClassNotice
    ClassSignoff
    ClassTail
    ClassRule
    ClassEditorial
)

Module is one compiled top-level module. Every module type implements it with pointer receivers, so Document.Modules holds *Statement, *Figure and so on. Kind names a module. Lookup maps a component name or handoff alias to its kind, case-insensitively. Class groups modules for layout spacing.

TypeKind constantValueAliasClassFields
StatementKindStatementstatementk1HeroAt, Eyebrow, Orange, XL, Blocks
FigureKindFigurefigurek2HeroAt, Eyebrow, Amount, USD, Blocks
OfferKindOfferoffere3HeroAt, Eyebrow, Heading, Terms
ImageKindImageimagek3BandAt, Src, Alt, Height, Tone
MapKindMapmapt4BandAt, Src, Alt, Height
LetterKindLetterletterb1BodyAt, Blocks
MetaKindMetametat3BodyAt, Pairs []MetaPair
QuoteKindQuotequotee2BodyAt, Cite, Body
NoticeKindNoticenoticeb2NoticeAt, Title, Warn, Blocks
CodeKindCodecodeb3TailAt, Value, Blocks (at most one paragraph)
ProgressKindProgressprogresst2TailAt, Steps, StepsRef, Current, CurrentRef, CurrentIndex
ActionsKindActionsactionsc1, c2, c3TailAt, Row ActionRow
BreakdownKindBreakdownbreakdownb4TabularAt, Rows
CompareKindComparecompareb5TabularAt, Header, Rows, Align
TableKindTabletableTabularAt, Headless, Header, Rows, Align
ReceiptKindReceiptreceiptt1TabularAt, Order data.Accessor, TotalLabel
StockKindStockstockt5TabularAt, Items data.Accessor
StoriesKindStoriesstoriese1EditorialAt, Items []Story
ProductsKindProductsproductse4EditorialAt, Items []ProductTile, ItemsRef, CTA
SignatureKindSignaturesignatures1SignoffAt, Name, Role, Contact
SignoffKindSignoffsignoffs2SignoffAt, Closing, Name (empty: "The {brand} team")
DividerKindDividerdividerRuleAt

Each module type has the three Module methods, Kind(), Class() and Pos(). The parts of modules:

TypeIs
MetaPairOne ### Label and its paragraph: Label, Body
StoryOne :::story tile: At, Eyebrow, Image, Alt, Href, CTA, Tone, and Title (its body)
ProductTileOne :::product tile: At, Name, Price, Image, Alt, Href, CTA, Tone
ActionA :button (Primary) or :link (Arrow) in an action row: Label, Href, Pos
RowOne table row, []Inline, one per column
AlignA GFM column alignment: AlignNone, AlignLeft, AlignCenter, AlignRight

Blocks and inline content

type Block interface{ /* unexported */ }       // Heading, Para, ActionRow or List
type InlineNode interface{ /* unexported */ }  // TextRun, Strong, Em, CodeSpan, Link, Span or Break
type Inline []InlineNode
TypeIs
HeadingLevel 1–6 and Body Inline
ParaA paragraph, Body Inline
ActionRowA paragraph made only of :button and :link: Items []Action
ListA bulleted or Ordered list, one paragraph per item: Items []Inline
TextRunText with merge tags, Value Text. Renderers escape merge values and never parse them as Markdown.
Strong, Em**strong** and *emphasis*, Body Inline
CodeSpan`code`, Value Text. It may hold merge tags.
Link[body](href), an autolink or a :link in running text: Href Text, Body Inline
Span[body]{.class …}: Style SpanStyle, Body Inline
BreakA line break, Hard or soft
type SpanStyle uint8

const (
    Muted SpanStyle = 1 << iota // .muted
    Teal                        // .teal
    Mono                        // .mono
    Small                       // .small
    Bold                        // .bold
)

func (s SpanStyle) Has(x SpanStyle) bool

SpanStyle is a set of span classes, one flag per allowed class. Has reports whether s includes every style in x.

Text and evaluation

type Text []Part

type Part struct {
    Lit string
    Ref *Ref
}

type Ref struct {
    Path        string
    Fallback    string
    HasFallback bool
    Pos         Pos
    Get         data.Accessor // precompiled; indexes Env.Roots
    Placeholder bool          // a {name} placeholder rather than a {{path}} merge tag
}
func (r *Ref) String() string // {{path}} or {name}

type Env struct {
    Name  string          // template name, for error positions
    Roots []reflect.Value // [data, recipient, vars], indexed by the Slot constants
}

const (
    SlotData      = 0
    SlotRecipient = 1
    SlotVars      = 2
)

func Lit(s string) Text
func (t Text) Literal() (string, bool)
func (t Text) Eval(env Env) (string, error)
func (t Text) EvalOptional(env Env) (string, error)
func (t Text) EvalURL(env Env) (string, error)
func (t Text) EvalURLOptional(env Env) (string, error)
IdentifierDoes
TextLiteral text with references resolved at compile time. Adjacent literals are always merged.
PartOne piece of a Text: a literal (Ref == nil) or a reference.
RefA compiled merge tag or copy placeholder.
EnvThe data a Text is evaluated against.
SlotData, SlotRecipient, SlotVarsThe indexes of Env.Roots, which is always [data, recipient, vars].
LitA literal Text. The empty string gives nil.
LiteralThe text and true when t has no references.
EvalRenders t. A reference with no value (unresolvable, or formatting to "") uses its fallback. Without one it is an *Error "no value for /reference/api-reference".
EvalOptionalLike Eval, but a missing value with no fallback gives "". Used for an optional image or map src.
EvalURLEval for an href or src. A reference that opens the text is inserted as is, so {{track_url}} and {site}/help work. Every later value, fallback included, is percent-escaped for its place: as a query value once the URL has a ? (spaces as %20), otherwise as a path segment, so mailto:{{email}} keeps its address. A later reference inside the authority (after //, before any /, ? or #) is not escaped. It must be a single DNS label, immediately preceded by literal // or ., and immediately followed by literal text starting with ., as in https://{{shop}}.example.com/. Anything else — including no value there, even in EvalURLOptional — is an *Error at the reference, "merge value /reference/api-reference cannot form part of a url host". A value can't add a query parameter, change the path or fragment, or move the host.
EvalURLOptionalEvalURL with EvalOptional's missing-value rule.

Import lttr/layout

import "github.com/sulv-io/lttr/layout"

Arranges compiled modules into a document the renderers draw: header and footer chrome, and the padding above and below every module. See Layout and spacing.

func Arrange(mods []ir.Module, header HeaderVariant) []Section

type Section struct {
    Module            ir.Module
    PadTop, PadBottom int
}

type Document struct {
    Name               string // template name, for render errors
    Subject, Preheader string
    Header             Header
    Sections           []Section
    Footer             Footer
    Theme              theme.Theme
    AllowRelativeURLs  bool
}
IdentifierDoes
ArrangeLays modules into sections with their padding. Spacing is decided only here, in three passes: class defaults, then the pair rules, then first-module, last-module and header rules. It doesn't modify mods.
SectionOne module with the padding (px) above and below it.
DocumentA laid-out email, ready for the HTML and text renderers.
type Header struct {
    Variant     HeaderVariant
    Banner      string // H2 banner text
    Masthead    string // H3 masthead text
    LogoURL     string
    LogoDarkURL string
}

type Footer struct {
    Variant  FooterVariant
    Nav      []ir.Action // F1 nav row; empty = no row
    Lead     ir.Inline   // F1 reason · F2 note · F3 opt-out · F3·i internal line
    Links    ir.Inline   // F1 unsubscribe and preferences links
    Service  ir.Inline   // F2 service line; empty = dropped
    Address  ir.Text     // "{brand} · {address}"
    TextURLs []TextURL   // plain-text footer URL lines, in order
}

type TextURL struct {
    Label  string
    URL    ir.Text
    Source string // the copy it was written in, naming render errors
}

type HeaderVariant uint8

const (
    H1 HeaderVariant = iota + 1 // logo
    H2                          // logo with the internal banner
    H3                          // newsletter masthead
)

type FooterVariant uint8

const (
    F1         FooterVariant = iota + 1 // marketing: nav, reason, unsubscribe and preferences
    F2                                  // service: note, "service email about …", address
    F3                                  // outreach: address and opt-out
    F3Internal                          // internal: internal line and address
)

Header is the evaluated header chrome. Footer is the footer chrome as compiled copy, evaluated by the renderers against the message Env. TextURL is one plain-text footer line. The root package re-exports the variants as lttr.H1… and lttr.F1….

type Rule struct {
    Prev, Next          Selector
    PrevBottom, NextTop *int
    Source              string // the handoff frame the value comes from, e.g. "Q2 (D10)"
}

type Selector struct {
    Class ir.Class
    Kind  ir.Kind // empty matches any kind of Class
}

Rule overrides the class defaults for one adjacent pair of sections, and Selector matches a module by class and, optionally, kind. The rule table itself is unexported.

Import lttr/theme

import "github.com/sulv-io/lttr/theme"

The brand values a renderer draws with. A leaf package: it imports only the standard library. See Themes.

type Theme struct {
    Light Light
    Dark  Dark
    Fonts Fonts
    Brand Brand
}

func (t Theme) Validate() error

type Color string
func (c Color) Valid() bool

type Fonts struct {
    Serif, Sans, Mono string
}

type Brand struct {
    Name     string // header text, logo alt and the default sign-off
    Logo     string // file name under Config.AssetBase, e.g. "logo.png" (72×72, shown at LogoSize)
    LogoDark string // white-chip version for dark mode
    LogoSize int    // display size in CSS pixels, 16..96
}
IdentifierDoes
ThemeEverything brand-specific a renderer needs.
Theme.ValidateReports every invalid value, one line per problem, or nil: colours must be #RRGGBB, font stacks non-empty and made only of [A-Za-z0-9 ,'-], Brand.Name non-empty, logo names plain file names, and LogoSize 16 to 96.
ColorA hex colour, #RRGGBB. Valid checks exactly that.
FontsThe three CSS font stacks.
BrandThe sender's mark.
type Light struct {
    Ink, Body, Secondary, StrongBody Color
    Action, PillText, Accent         Color
    Hairline, RowRule, NoticeBorder  Color
    Ground, Canvas                   Color
    WarnBg, WarnBorder, WarnText     Color
    OutOfStock                       Color
    BannerBg, BannerText             Color
    Warm, Sea, Map, Sun              Color
}

type Dark struct {
    Canvas, Footer, Hairline     Color
    Headline, Body               Color
    Action, PillText             Color
    CurrentStep, AccentText      Color
    BannerBg, BannerText         Color
    Placeholder                  Color
    WarnBg, WarnBorder, WarnText Color
    OutOfStock                   Color
}

Light is the light-mode palette. Dark is the dark-mode palette, applied by the head CSS under prefers-color-scheme: dark and by the Outlook.com data-ogsc/data-ogsb hooks.

Import lttr/render/html

import htmlr "github.com/sulv-io/lttr/render/html"

Renders a laid-out document as Outlook-safe, table-based HTML. The package is named html, so import it under an alias if you also use the standard library's html or html/template. See HTML output.

func Render(doc layout.Document, env ir.Env) (string, error)
func CheckURL(raw string, allowRelative bool) (template.URL, error)

type Pill struct {
    Href       template.URL
    Label      string
    Fill, Text theme.Color
    Width      int // estimated width in px; zero means computed from Label
}

type Style struct{ /* unexported */ }
func (s Style) CSS() template.CSS
IdentifierDoes
RenderRenders a complete HTML email. It returns the HTML or an error, never both.
CheckURLVets a URL before it may appear in an href or src. Allowed schemes are https, http, mailto and tel, and relative URLs only with allowRelative. Whitespace or a control character anywhere is an error, and a bad URL is always an error, never a substituted #. It returns the URL unchanged as template.URL.
PillA call-to-action button as Outlook draws it: a VML rounded rectangle. Href must already have passed CheckURL.
StyleAn inline style built only from package constants and validated theme colours. Its builders are unexported, so no caller can put arbitrary text into a style attribute.
Style.CSSThe declarations as prop:value;prop:value, typed for html/template.

Import lttr/render/text

import "github.com/sulv-io/lttr/render/text"
func Render(doc layout.Document, env ir.Env) (string, error)

Renders the plain-text part from the laid-out document, working from the typed IR rather than the HTML: blocks separated by one blank line, 70-column wrapping, uppercase eyebrows, Label: plus URL for actions, and dot-leader tables. It returns the text or an error, never both. See Plain text.

Import lttr/mapbox

import "github.com/sulv-io/lttr/mapbox"

Builds Mapbox Static Images API URLs for the map band and fetches the image. The API URL carries your access token, so it must never go into an email. Fetch the image at send time, upload it to your CDN, and put only the CDN URL in ::map{src}. See Maps.

type Static struct {
    Token   string
    Style   string // owner/id map style
    Width   int    // default 600
    Height  int    // default 240
    Padding int    // default 80

    RouteColor   string // route line stroke; required, six hex digits without '#'
    PickupColor  string // pickup pin; required
    DropoffColor string // drop-off pin; required
}

func (s Static) URL(pickup, dropoff Point, route []Point) (string, error)
func (s Static) Fetch(ctx context.Context, c *http.Client, pickup, dropoff Point, route []Point) ([]byte, error)

type Point struct {
    Lng, Lat float64
}
IdentifierDoes
StaticBuilds Static Images API requests. The three colours are the brand's, so the package has no defaults for them. quiet.Mapbox(token, style) shows one set up.
Static.URLThe API URL for a pickup, a drop-off and a route. With an empty route, the line runs straight from pickup to drop-off. No error it returns contains the token.
Static.FetchGETs the image and returns its bytes. A non-200 response is an error naming only the status code, never the URL, and a body over 8 MiB is an error.
PointA coordinate: longitude, latitude in decimal degrees.
Copyright © 2026