Markdown and Review Templates
afmail uses one MiniJinja template system for generated Markdown and Review HTML. Both outputs receive the same versioned, typed render documents. Review contains those documents directly; it does not rebuild a second HTML-shaped copy of mail, collection, draft, notes, suggestion, or attachment data.
Workspace Paths
Run:
afmail render templates
The command exports editable overrides under templates/. Language-specific
entry templates live under the resolved BCP 47 language:
templates/<language>/
i18n/catalog.j2
message/section.md.j2
triage/view.md.j2
case/case.md.j2
case/message.md.j2
notification/notification.md.j2
notification/message.md.j2
status/index.md.j2
status/message.md.j2
draft/new.md.j2
draft/reply.md.j2
notes/default.md.j2
notes/merge-section.md.j2
workspace/AGENTS.md.j2
workspace/DO_NOT_EDIT.txt.j2
review/page.html.j2
review/message.html.j2
review/draft-editor.html.j2
review/notes-editor.html.j2
Shared, language-independent layouts and CSS live under:
templates/shared/
markdown/*.md.j2
review/*-layout.html.j2
review/style.css.j2
Built-in packs are complete for en-US, de-DE, es-ES, fr-FR, ja-JP,
ko-KR, pt-BR, zh-CN, and zh-TW. workspace.language_bcp47 selects the
closest pack. The locale catalog owns translated copy, punctuation, and
language-specific macros; these are templates, not fields injected into render
data.
A language entry template normally imports its catalog and includes a shared
layout. That split is only a default composition. An override may replace the
entire language entry template, replace a shared layout, introduce additional
partials, change ordering and visibility, or use entirely different Markdown
or HTML structure. This keeps per-language freedom without duplicating data.
An additional include resolves under templates/<language>/ first and then
templates/shared/; use a shared/... include path to target the shared tree
explicitly. Absolute paths and parent-directory traversal are rejected.
afmail render refresh rebuilds generated Markdown after an edit. Review
templates are loaded when a Review page is rendered, so they do not require a
Markdown refresh. If a selected workspace override exists but is invalid,
afmail returns template_render_failed; it does not silently substitute a
built-in template.
Render Document Contract
The current render-document contract is schema version 2. Its Rust definitions
live in src/render_document.rs.
MessageRenderDocument contains:
- one exact canonical
MessageFileundermessage; - output-independent analysis such as direction, reply/forward classification, quote ranges, and security conclusions;
- content trust and provenance.
Message body text exists only once, at message.body_text. Quote analysis
contains byte ranges into that string:
{
"message": {
"message_id": "message_...",
"body_text": "Authored text\n\n> quoted text"
},
"analysis": {
"direction": "inbound",
"action": "reply",
"body": {
"segments": [
{"kind": "authored", "start_byte": 0, "end_byte": 13},
{"kind": "quoted", "start_byte": 15, "end_byte": 28}
]
}
}
}
Use the text_range filter to read a segment. Drafts follow the same rule:
their full body exists once and authored_body_range identifies the editable
compose text.
Collection documents contain one canonical collection plus contained
MessageRenderDocument values. Triage documents contain the message document,
its relationships, and canonical suggestion facts. Review then contains these
same documents:
- triage Review:
contents.items[]is the exactTriageRenderDocumentused by the corresponding Markdown view; - case/notification Review:
contents.collectionisCollectionRenderDocument; - draft Review:
contents.draft.documentisDraftRenderDocument; - notes editors contain
NotesRenderDocument.
The editable wrappers add only operational state such as a base hash. They do not copy or rename the contained domain fields.
The remaining language-scoped Markdown scaffolds also receive raw facts, not
pre-rendered fragments. In particular, draft/reply.md.j2 receives nullable
sender and one raw quoted_body; the built-in template applies
markdown_quote. notes/merge-section.md.j2 receives case_uid,
other_case_uid, and raw other_body. Workspace instruction templates receive
the canonical workspace paths they need.
Render data must remain output-neutral. Do not add localized copy, fallback labels, symbols, CSS classes, formatted dates or byte sizes, escaped strings, display aliases, layout rows, or pre-rendered Markdown/HTML. Templates and their filters own wording, localization, fallback, pluralization, formatting, escaping, markup, ordering, visibility, and layout.
Useful built-in filters include datetime, bytes, text_range,
chronological, notification_date_desc, message_item, message_status_key,
display_name, initials, asset_path, and the Markdown filters inline,
plain, markdown_code, markdown_alt, markdown_fence, markdown_quote,
markdown_table, and yaml.
Review HTML and JavaScript
Workspace Review overrides may contain MiniJinja, HTML, and CSS. They may not
contain JavaScript, inline event handlers, JavaScript URLs, active embedded
content, disabled autoescaping, or safe output. afmail validates every
workspace Review template and locale catalog, then validates the composed
Review HTML again after rendering. Review forces HTML autoescaping for every
included partial regardless of its filename suffix.
afmail injects one trusted, behavior-only runtime after server-side rendering.
It performs autosave, submit/cancel, conflict handling, required-confirmation
checks, and draft-tab switching. It does not construct page markup and it gets
all visible status/error copy from data-copy-* attributes rendered by the
language template.
A fully custom Review page can rearrange the UI while retaining behavior by
preserving the relevant data-afmail-* hooks from the built-in templates.
Forms remain ordinary server-rendered HTML: autosave forms provide their
action, base_hash, and canonical field names; decision controls opt into
submission with data-afmail-submit-field.
Editing and Verification
- Export templates with
afmail render templates. - Edit the selected language entry, its
i18n/catalog.j2, or a shared layout. - Run
afmail render refreshfor Markdown changes. - Run
afmail doctorto compile and smoke-render every required workspace template.
Use afmail render templates --force only when intentionally replacing all
existing workspace overrides with the current built-ins.