Letterpress has two separate jobs: compile a template while an operator is authoring it, then render the stored artifact when a notification is sent. Source analysis, compilation, formatting, and translation run through the bundled Node worker; only the email profile invokes MJML. Delivery rendering is pure BEAM.
Add Letterpress to your dependencies:
{:letterpress, "~> 0.1"}On nodes that compile templates, add a compiler pool to the host application's supervision tree:
children = [
{Letterpress.Compiler.Supervisor, pool_size: 2}
]
Supervisor.start_link(children, strategy: :one_for_one)Letterpress does not start processes automatically. Delivery-only nodes omit this child.
Define an explicit schema. Contexts describe where a value may be emitted; they are security rules, not documentation labels.
The text, url, and compile-phase color contexts support the documented
compatible email sinks, so the same typed value can safely appear in HTML,
subject, and plain-text alternatives while each occurrence receives its
sink-specific validation and escaping.
schema = %{
"version" => 1,
"variables" => %{
"html_name" => %{"type" => "string", "context" => "html_text"},
"subject_name" => %{"type" => "string", "context" => "subject"},
"text_name" => %{"type" => "string", "context" => "text"},
"action_url" => %{"type" => "url", "context" => "url"}
}
}
source = """
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello {{ html_name }}</mj-text>
<mj-button href="{{ action_url }}">Open account</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
"""
{:ok, artifact, diagnostics} =
Letterpress.compile("email/mjml-liquid@1", source, schema,
subject: "Welcome, {{ subject_name }}",
text: "Hello {{ text_name }}. Open {{ action_url }}"
)Persist the complete artifact. At delivery, decode persisted JSON if needed and render all channels atomically:
{:ok, result} =
Letterpress.render(artifact, %{
"html_name" => "Taylor",
"subject_name" => "Taylor",
"text_name" => "Taylor",
"action_url" => "https://example.test/account"
})
send_email(result.subject, result.html, result.text)Do not recompile in a queue worker and do not fall back to a permissive renderer after a compiler failure. A publication without a valid artifact is not deliverable.