PhoenixKit.Email.Layout (phoenix_kit v2.43.0)

Copy Markdown View Source

The shared HTML layout every email built from a file or a default is wrapped in.

PhoenixKit.Email.Content.resolve/5 calls wrap/3 on the file/default path, so an email reaches the reader as HTML even when all it has is text: core's auth emails and the defaults a module passes to PhoenixKit.Mailer.send_from_template/4 ship no html part of their own. Content from a database template is never wrapped — it carries its own chrome.

The default is a function, like every other default

default_html/0 is core's layout, kept in code for the same reason the message defaults are Gettext calls rather than files: core ships no files of its own. It is deliberately neutral — the site's name above the body, the name and a link to the site below it, table markup with inline styles, no brand colours and no external resources. It carries no words of its own, so it needs no translation: everything it prints is the site's name, its URL and the message itself.

Overriding it

The layout is resolved like any other template, under the reserved name _layout, so a host replaces it with an ordinary override file:

<host>/priv/phoenix_kit_templates/
└── _layout/
    ├── html.html          <- every locale
    └── html.de.html       <- German readers

Locale selection, roots and caching are exactly those of the message itself (PhoenixKit.Templates). The layout's subject and text parts are never read. Finding a file under a name that starts with _ needs phoenix_kit_templates 0.2.1 or later; older releases skip such names and core's default is used.

A layout with no content placeholder (an empty file, a typo such as {{{contnet}}}) would drop the body of every email, so it is refused: core's default is used instead, and a warning is logged once per override roots and locale for the life of the VM.

Variables

placeholdervalue
{{{content}}}the message body, already HTML
{{subject}}the message subject, for <title>
{{site_name}}PhoenixKit.Settings.get_project_title/0
{{site_url}}PhoenixKit.Utils.Routes.base_url/0

site_url is whatever the site is configured with. Core's default links it only when it is an http(s):// address and prints it as text otherwise.

content must be written with three braces. The html part escapes every {{variable}}, which is right for subject and the site's name but would print the body's markup as visible text. Triple braces are the escaping opt-out, and the body is safe to insert raw because it was escaped when it was built — by the html part's own substitution, or by text_to_html/1.

Opting out

layout: false to Content.resolve/5 or Mailer.send_from_template/4 leaves the message exactly as resolved. An html part that is already a whole document is never wrapped either — an export of an old database template is one. It is a document when, after any byte-order mark, whitespace, <!-- comments --> and <?xml … ?> prolog, it starts with <!doctype or an <html tag (<html>, <html lang="…">), in any case.

Summary

Functions

Core's layout: a header with the site's name, the body, and a footer with the name and a link to the site.

Whether html is a whole document rather than a fragment to wrap.

The reserved template name the layout resolves under.

Plain text as an HTML fragment.

Wraps an HTML fragment in the layout for this message's locale.

Functions

default_html(opts \\ [])

@spec default_html(keyword()) :: String.t()

Core's layout: a header with the site's name, the body, and a footer with the name and a link to the site.

Options

  • :locale — becomes the document's lang attribute when it is a well-formed language tag (pt_BR is written pt-BR); omitted otherwise.
  • :link — false prints {{site_url}} in the footer as text instead of a link. Default true.

document?(html)

@spec document?(String.t()) :: boolean()

Whether html is a whole document rather than a fragment to wrap.

Leading byte-order marks, whitespace (non-breaking included), comments and an XML prolog are skipped before looking for <!doctype or <html.

name()

@spec name() :: String.t()

The reserved template name the layout resolves under.

text_to_html(text)

@spec text_to_html(String.t()) :: String.t()

Plain text as an HTML fragment.

Every character of text is escaped. A blank line separates paragraphs, each a <p>; a single line break becomes <br>. An http:// or https:// address becomes a link. It ends at <, >, ", any Unicode whitespace or separator, any invisible format character (zero-width spaces and joiners, the soft hyphen, the byte-order mark, bidirectional controls — which could otherwise make a link display a different address from the one it opens), guillemets, the em dash and the punctuation after it up to the ellipsis (U+2014–U+2027: typographic quotes, … and the like), and CJK/fullwidth punctuation. Hyphens and the en dash do not end it, since real addresses carry them. Trailing sentence punctuation and unbalanced closing parentheses stay outside it. No other scheme becomes a link, so javascript: and friends remain text, and neither does an address longer than 2048 bytes.

wrap(content, subject, opts \\ [])

@spec wrap(String.t(), String.t() | nil, keyword()) :: String.t()

Wraps an HTML fragment in the layout for this message's locale.

subject may be nil. Options are the message's own :locale and :paths, so the layout resolves from the same override roots and for the same reader as the message it wraps.