A Clojure library for rendering MJML email templates to HTML using the excellent mjml4j library.
Add to your deps.edn:
{:deps {com.github.kanwei/clj-mjml {:mvn/version "0.1.0"}}}
Or to your project.clj:
[com.github.kanwei/clj-mjml "0.1.0"]
(require '[clj-mjml.mjml :as mjml])
;; Basic rendering
(def html (mjml/render "<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello World!</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>"))
(require '[clj-mjml.mjml :as mjml])
;; Simple template
(mjml/render "<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size=\"20px\">Hello World</mj-text>
<mj-button href=\"https://example.com\">Click Me</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>")
;; => "<!doctype html><html>...</html>"
Split your templates into reusable components with mj-include:
;; Load templates from classpath (resources directory)
(def resolver (mjml/create-classpath-resolver))
(mjml/render "<mjml>
<mj-body>
<mj-include path=\"templates/header.mjml\" />
<mj-section>
<mj-column>
<mj-text>Email content here</mj-text>
</mj-column>
</mj-section>
<mj-include path=\"templates/footer.mjml\" />
</mj-body>
</mjml>"
{:mj-include-resolver resolver})
Render emails with specific language and text direction:
;; French email
(mjml/render template {:language "fr"})
;; Arabic email with right-to-left text
(mjml/render template
{:language "ar"
:text-direction :rtl})
;; Combine with includes
(mjml/render template
{:mj-include-resolver (mjml/create-classpath-resolver)
:language "es"
:text-direction :ltr})
The render function accepts an options map:
(mjml/render template {
:mj-include-resolver resolver ; Handle mj-include tags
:language "en" ; Set lang attribute (default: "en")
:text-direction :ltr ; :ltr or :rtl (default: :ltr)
})
clj-mjml provides three types of include resolvers for loading template partials:
Load templates from your project's resources directory (or JAR files):
(def resolver (mjml/create-classpath-resolver))
;; Directory structure:
;; resources/
;; templates/
;; header.mjml
;; footer.mjml
;; partials/
;; navigation.mjml
(mjml/render template {:mj-include-resolver resolver})
Load templates from the filesystem:
;; With specific base path
(def resolver (mjml/create-filesystem-resolver "/path/to/templates"))
;; Or use current directory as base
(def resolver (mjml/create-filesystem-resolver))
(mjml/render template {:mj-include-resolver resolver})
Implement your own loading logic:
;; Load from database
(def resolver
(mjml/create-custom-resolver
(fn [path]
(db/fetch-template-by-name path))))
;; Load from memory
(def templates {"header" "<mj-section>...</mj-section>"
"footer" "<mj-section>...</mj-section>"})
(def resolver
(mjml/create-custom-resolver
(fn [path] (get templates path))))
(mjml/render template {:mj-include-resolver resolver})
(ns my-app.email
(:require [clj-mjml.mjml :as mjml]))
(def resolver (mjml/create-classpath-resolver))
(defn render-order-confirmation [order]
(mjml/render
(str "<mjml>
<mj-head>
<mj-title>Order Confirmation</mj-title>
</mj-head>
<mj-body>
<mj-include path=\"templates/header.mjml\" />
<mj-section>
<mj-column>
<mj-text font-size=\"20px\">Order #" (:id order) "</mj-text>
<mj-text>Thank you for your purchase!</mj-text>
<mj-button href=\"" (:tracking-url order) "\">
Track Order
</mj-button>
</mj-column>
</mj-section>
<mj-include path=\"templates/footer.mjml\" />
</mj-body>
</mjml>")
{:mj-include-resolver resolver}))
(defn render-welcome-email [user]
(let [lang (:language user)
dir (if (contains? #{"ar" "he"} lang) :rtl :ltr)
template-path (str "templates/welcome-" lang ".mjml")]
(mjml/render
(str "<mjml>
<mj-body>
<mj-include path=\"" template-path "\" />
</mj-body>
</mjml>")
{:mj-include-resolver (mjml/create-classpath-resolver)
:language lang
:text-direction dir})))
(def resolver (mjml/create-filesystem-resolver "./newsletter-templates"))
(defn render-newsletter [articles]
(let [article-sections
(map #(str "<mj-include path=\"articles/" (:id %) ".mjml\" />")
articles)]
(mjml/render
(str "<mjml>
<mj-body>
<mj-include path=\"branding/header.mjml\" />
<mj-include path=\"branding/navigation.mjml\" />
" (clojure.string/join "\n" article-sections) "
<mj-include path=\"branding/footer.mjml\" />
</mj-body>
</mjml>")
{:mj-include-resolver resolver})))
When includes cannot be loaded:
<!-- mj-include fails to read file: path -->render(render template)
(render template options)
Renders MJML to HTML.
Arguments:
template - MJML template stringoptions - Optional map with keys:
:mj-include-resolver - Include resolver for handling mj-include tags:language - Language code (default: "en"):text-direction - :ltr or :rtl (default: :ltr)Returns: HTML string
create-classpath-resolver(create-classpath-resolver)
Creates a resolver that loads includes from the classpath.
create-filesystem-resolver(create-filesystem-resolver)
(create-filesystem-resolver base-path)
Creates a resolver that loads includes from the filesystem.
Arguments:
base-path - String or File representing the base directory (optional, defaults to current directory)create-custom-resolver(create-custom-resolver loader-fn)
Creates a custom resolver using a provided loader function.
Arguments:
loader-fn - Function (fn [path] => mjml-string) that loads templates$ clojure -T:build test
Copyright © 2025 Kanwei Li
Distributed under the MIT License.
Run the project's CI pipeline and build a JAR:
$ clojure -T:build ci
This will produce an updated pom.xml file with synchronized dependencies inside the META-INF
directory inside target/classes and the JAR in target. You can update the version (and SCM tag)
information in generated pom.xml by updating build.clj.
Install it locally (requires the ci task be run first):
$ clojure -T:build install
Deploy to Clojars -- needs CLOJARS_USERNAME and CLOJARS_PASSWORD environment variables (requires the ci task be run first):
$ clojure -T:build deploy
Your library will be deployed to com.github.kanwei/clj-mjml on clojars.org.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |