mjml-hamlet: MJML email templates in Hamlet syntax, checked at compile time

[ email, library, mit, text, web ] [ Propose Tags ] [ Report a vulnerability ]

A quasiquoter for MJML documents written with Hamlet's indentation-based syntax and interpolation. Unknown elements, misplaced elements, undeclared attributes and attribute values of the wrong type are compile errors.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.1.0.1
Change log CHANGELOG.md
Dependencies base (>=4.20 && <5), blaze-markup (>=0.8 && <0.9), containers (>=0.6 && <0.9), megaparsec (>=9.6 && <9.9), mjml (>=0.1 && <0.2), parsec (>=3.1 && <3.2), shakespeare (>=2.1 && <2.3), template-haskell (>=2.22 && <2.25), text (>=2.0 && <2.2) [details]
Tested with ghc ==9.10.3 || ==9.12.4 || ==9.14.1
License MIT
Copyright 2026 Kronor
Author José Lorenzo Rodríguez
Maintainer lorenzo@kronor.io
Uploaded by lorenzo at 2026-10-31T09:05:43Z
Category Text, Web, Email
Distributions
Downloads 6 total (6 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for mjml-hamlet-0.1.0.0

[back to package description]

mjml-hamlet

MJML email templates written in Hamlet's syntax and checked at compile time. Rendering goes through the mjml package, so there is no Node.js involved at build time or at runtime. For what MJML is and a full example with screenshots, see the repository README.

{-# LANGUAGE QuasiQuotes #-}

import Text.Mjml.Hamlet (mjml, renderMjml)

shipped :: Order -> Either MjmlError Text
shipped order =
    renderMjml
        [mjml|
        <mj-section>
          <mj-column>
            <mj-text font-size="20px">Your order #{order.reference} is on its way
            $forall line <- order.lines
              <mj-text>#{line.name} × #{line.quantity}
            $maybe url <- order.trackingUrl
              <mj-button href=#{url} .primary>Track it
        |]

Syntax

As in Hamlet, nesting is given by indentation and there are no closing tags.

  • <mj-text align="center">Hi is an element; text after the tag on the same line is its content. Attribute values may be quoted, unquoted, or an interpolation (href=#{url}). .name adds a class (css-class on MJML elements, class on HTML ones).
  • #{expr} interpolates any ToMarkup value, escaped. Expressions are Hamlet's: variables, application, operators, literals and record fields (order.lines), but no type annotations or lambdas.
  • ^{expr} embeds what was built elsewhere: nodes (Node, [Node], Maybe Node) among MJML elements, or blaze Markup inside an ending tag.
  • $forall pattern <- expr, $if / $elseif / $else, $maybe pattern <- expr / $nothing, $case expr with nested $of pattern lines, and $with pattern <- expr work as in Hamlet. $# starts a comment line.
  • Inside an ending tag (mj-text, mj-button, mj-raw, mj-table, …) the nested lines are HTML. Consecutive text lines are joined with a space; a leading \ keeps a line's leading space.

mjmlFile "templates/shipped.mjml" reads the same syntax from a file and recompiles when the file changes.

A template is a list of nodes. renderMjml renders a whole document rooted at <mjml> as it is, wraps a template made of <mj-head> and <mj-body> in <mjml>, and wraps anything else in <mjml><mj-body>, so a template can be just the sections of an email.

Escaping and injection

#{...} escapes <, >, &, " and ', so an interpolated value cannot leave its text or attribute. Because MJML copies many attributes into CSS and some are URLs, interpolated attribute values are also checked for where they are going:

  • URLs (href, src, background-url, …) take either a single #{url} of type SafeUrl, which safeUrl only builds for https, http and mailto URLs with no quotes or spaces in them, or a literal that fixes the scheme, as in href="https://example.com/o/#{ref}", where the interpolated part is percent-encoded. Anything else does not compile.
  • Typed attributes (colours, units, enumerations) must match their type when the email is rendered.
  • Other attributes that end up in CSS, and style on HTML elements, must not contain anything that could add a declaration: ;, braces, backslashes, comments, url( or @import.
  • Event handlers (onclick, …) cannot be interpolated at all.

A value that fails at render time is dropped, as if the attribute had not been written. ^{...} embeds markup without escaping and cannot be used in attribute values.

What does not compile

The checks MJML's validator makes at runtime happen at compile time, with the template's line and column:

Shipped.hs:12:39: Attribute align has invalid value: middle for type Enum, only accepts left, right, center, justify
  • an unknown element, or HTML outside an ending tag;
  • an element inside a parent MJML does not allow (mj-text directly in mj-section);
  • an attribute the element does not declare;
  • a literal attribute value of the wrong type (units, colours, enumerations);
  • text outside an ending tag.

License

MIT.