Rendering

Plain text

How lttr writes the plain-text part from the same compiled template, and the rules every text part follows.

Every message carries a plain-text part in Message.Text, beside the HTML. lttr builds it from the compiled template, module by module, with github.com/sulv-io/lttr/render/text. It never strips tags from the HTML, so the text part reads as if someone had written it by hand: actions are labelled, tables line up, and the chrome is reduced to what a text reader needs.

Read a text part

This is the text part of the Quiet order email, byte for byte from testdata/golden/:

01-order.out_for_delivery.txt
CARIBHUBS

ORDER CH-20417

On its way.

Kareem picked up your order at 2:14 pm and will reach Frigate Bay by
5:30 pm.

Track delivery:
https://caribhubs.com/orders/CH-20417

Confirmed ✓ > Packed ✓ > [On its way] > Delivered

Mahogany serving board x1 ....... EC$185
Scotch bonnet pepper sauce x2 ... EC$56
Delivery, paid to your driver ... EC$15
Total ........................... EC$256
                                 ≈ US$95

DELIVER TO
Keisha Browne
12 Frigate Bay Road, St Kitts

DRIVER
Kareem J. · ★ 4.9
Grey Toyota Hiace

Payment is held until you confirm delivery. Questions? Reply to this
email or visit the Help Centre (https://caribhubs.com/help).

--
CaribHubs · Basseterre, St Kitts
Notification settings: https://caribhubs.com/preferences

It comes from this template: a statement with an eyebrow, a heading, a paragraph and a button; a progress; a receipt; a meta; and a buyer family F2 footer whose note the template overrides.

The email gallery is not available
public/gallery/manifest.json was not found when the site was built. Run `pnpm gallery` in docs-site/ (it needs Go).

Follow the layout rules

  • Blocks are separated by exactly one blank line, and the part ends with a newline.
  • Lines wrap at 70 characters on spaces. A word longer than that, such as a URL, sits on its own line unbroken.
  • The header is the brand name (Theme.Brand.Name) in capitals. An H2 family puts its banner in capitals on the line above; an H3 family puts its masthead in capitals on the line below.
  • Eyebrows, table header rows, meta labels, story eyebrows and quote cites are written in capitals.
  • Bold, italic and spans contribute their text only. Code spans contribute their value.
  • An action (a :button, a :link, a story or product link) is its label and a colon on one line and the full URL on the next. A trailing › on the label is dropped.
    01-order.out_for_delivery.txt
    Track delivery:
    https://caribhubs.com/orders/CH-20417
    
  • A link in prose is written label (url), or just the URL when the label is empty or is the URL itself. An email autolink such as <ops@acme.example> is written as the bare address.
  • Every URL is written in full, including merge values escaped as in the HTML (D34).

Line up tables with dot leaders

Two-column tables, breakdowns, receipts and stock lists use dot leaders:

02-payout.sent.txt
Sales · 14 orders ............... EC$1,085.00
Commission · 6% ................. −EC$65.10
Reference ....................... PO-0923-114
  • Values start in one column, at the longest label plus 5, clamped to columns 34–50; a label too long for that column still gets 3 dots, so its value starts further right.
  • A row is label + " " + dots + " " + value.
  • A sub-value, such as the ≈ US$ total of a receipt, sits on the next line, right-aligned to the end of the value.
  • A receipt line's label is Name xQty, with no x0 when the quantity is zero (D30).
  • A stock row is Name · SKU against N left or Out of stock.
  • A two-column table with a header row writes the header in capitals, padded with spaces instead of dots, so it reads as column headings (D30).

Tables with any other number of columns become padded columns, 3 spaces apart, each as wide as its widest cell. Columns the GFM table aligns right stay right-aligned:

07-fees.change.txt
COMMISSION       NOW   FROM 1 NOV
Goods             6%           6%
Services          4%         3.5%
Delivery fee   EC$15        EC$15

Know each module's text form

ModuleText
statementThe eyebrow, then each heading, paragraph, list and action as its own block
figureThe eyebrow; then the amount and its ≈ line as one block; then the copy
letterEach paragraph, list and action as its own block
noticeThe title and the text as one block; actions follow as their own blocks
codeThe code with its whitespace removed, then the copy
progressConfirmed ✓ > Packed ✓ > [On its way] > Delivered: done steps ticked, the current one in brackets
metaEach pair as a block: the label in capitals, then its lines, line breaks kept
receipt, breakdown, stock, two-column table and compareDot leaders
Wider table and comparePadded columns
storiesEach story as a block: eyebrow in capitals, title, then Read: (or its cta) and the URL
productsEach product as a block: Name · Price, then Buy: (or its cta) and the URL
quoteThe quote, then the cite in capitals
offerThe eyebrow, the heading and the terms as three blocks
actionsEach action as its own block
signatureName, role and contact as one block
signoffThe closing (default Thanks,) and the name (default The {brand} team) as one block
image, map, dividerNothing

Lists keep their markers: - item for bullets and 1. item for numbered lists, with a hanging indent when an item wraps (D30).

The footer is the lead line, a blank line, then a block starting with --:

05-campaign.heritage_week.txt
You're getting this because you subscribed to CaribHubs updates for St
Kitts & Nevis.

--
CaribHubs · Basseterre, St Kitts
Unsubscribe: https://caribhubs.com/unsubscribe
Preferences: https://caribhubs.com/preferences
  • The lead is the F1 reason, F2 note, F3 opt-out line or F3·i internal line, after any footer.* override.
  • Then --, the address line from the family's Address copy, and one Label: URL line per entry in the family's TextLinks. A link whose URL evaluates to empty is left out.
  • The F1 nav row, the F1 Links and the F2 service line have no text form. That is why the URLs a reader needs go in TextLinks (see Families and streams).

Check the text part

The text part is checked like the HTML: a merge tag with no value, an empty action URL or an empty bound list is a render error in both parts (D31). URLs shown in both parts are checked by the HTML renderer, and text-only footer URLs are checked by Render with the same rules before either part is written (D30, D34).

The nine Quiet text parts are goldens in testdata/golden/*.txt, compared byte for byte by go test ./examples/quiet. See Goldens and testing.

Copyright © 2026