Crafting Beautiful Emails in Elixir Using MJML

Posted by Alex Koutmos on Wednesday, January 20, 2021

Contents

Intro

In this blog post, we’ll be talking about what exactly MJML is, why it is an awesome tool for creating slick looking emails, how to build MJML templates during the Elixir compilation phase, and how we go about sending these beautiful emails using Swoosh. In order to make this a bit more real world, we’ll also be leveraging Phx.Gen.Auth and will craft a great looking welcome email to ensure our new users feel extra welcome to our new SaaS platform ;).

Without further ado, let’s dive right into things!

What is MJML?

MJML (https://mjml.io/) is a responsive email framework that ensures your emails are responsive and look consistent across the various email clients. If you thought that cross browser render parity was an issue…email client compatibility is an even bigger nightmare. MJML takes that headache away and allows you to craft slick looking emails in a few lines of XML markup. If you look at the MJML documentation (https://mjml.io/documentation/#components), you’ll also see that there are plenty of included components to help get you on your way.

After you have created your MJML template, you’ll need to go through a compilation step to convert that MJML template into a valid HTML document for email clients to render. If you want to use the 1st party tooling, you can download and use the NPM MJML (https://www.npmjs.com/package/mjml) package to convert MJML documents to HTML documents. In this post though, we’ll be leveraging a slightly different tool given that we don’t want to add any additional complexity to our build pipeline and would like it to be as streamlined as possible (i.e all operations take place in Elixir land).

Show me the code!

In order to perform all of our MJML compilation steps in the context of an Elixir compilation, we’ll be using the Elixir package https://github.com/adoptoposs/mjml_nif that actually wraps a Rust MJML library using Rustler (if that doesn’t have all the buzzwords required to get to the top page of HackerNews…I don’t know what does). For your FYI, the Rust MJML library that mjml_nif wraps is called mrml (https://github.com/jdrouet/mrml). Once mjml_nif converts the MJML template to an HTML document, we’ll then leverage an EEx macro to dynamically generate a function in a module using that compiled MJML document. We’ll also add an attribute to our module to hint to the Elixir compiler that our module needs to be recompiled any time the MJML file changes since our EEx macro will depend on that external file. While this all may sound a bit confusing, keep reading and you’ll actually see that we can accomplish all of this in less than 20 lines of code! With that all being said, let’s jump right into it!

Step 1: Create a new Phoenix project and install dependencies - commit

Install the Phoenix project generator (if you don’t already have it installed) by running:

$ mix archive.install hex phx_new 1.5.7

With that done and in place, we’ll want to generate a new Phoenix project. You can replace mjml_demo with what ever you want to call your project and can also re-enable generator features as you see fit. Run the following to generate your project:

$ mix phx.new mjml_demo --no-webpack

Once your project is created, open up your mix.exs file and modify your deps/0 function as follows:

def deps do
  [
    ...
    {:hackney, "~> 1.9"},
    {:swoosh, "~> 1.1.2"},
    {:mjml, "~> 0.2.0"},
    {:phx_gen_auth, "~> 0.6.0", only: :dev}
  ]
end

With all that in place go ahead and run mix deps.get to fetch all of your dependencies. Now on to setting up Phx.Gen.Auth (in Phoenix 1.6 you won’t have to do this separately as the authentication generator has been ported over to Phoenix itself https://github.com/phoenixframework/phoenix/pull/4077)!

Step 2: Setting up Phx.Gen.Auth - commit

Next, in a terminal session, we’ll run the Phx.Gen.Auth Mix Task to generate all of our authentication boilerplate. Given that this will also compile our application and dependencies, we’ll need to ensure that a valid Rust compiler is available in our PATH environment variable. For that purpose, I recommend setting up asdf. I personally use asdf for all my run-time versioning needs but do pick whatever works best for you :). You can get more information and set up instructions on asdf and the asdf Rust plugin from the following links: https://github.com/asdf-vm/asdf and https://github.com/code-lever/asdf-rust.

With that out of the way, let’s run the following:

$  mix phx.gen.auth Accounts User users --binary-id

Once that completes, you’ll want to run the following terminal commands as Phx.Gen.Auth needs to fetch some dependencies that were injected into your mix.exs file and also needs its database migrations run (make sure you have a Postgres instance running somewhere):

# If you need to start up Postgres, run the following in a separate terminal
$ docker run -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:12

$ mix deps.get
$ mix ecto.setup

With that in place, we’re ready to configure Swoosh. Let’s get to it!

Step 3: Configuring Swoosh - commit

Let’s start off by creating a directory that will act as our “email” context. In this directory we will house all the MJML templates, and their EEx module counterparts so that everything is nicely organized. Will call this directory emails and we’ll create it at lib/mjml_demo/emails/. Next we’ll create a module at lib/mjml_demo/emails.ex that will act as the entry point into our emails context. Our MjmlDemo.Emails module will have the following contents:

defmodule MjmlDemo.Emails do
  use Swoosh.Mailer, otp_app: :mjml_demo

  import Swoosh.Email

  alias __MODULE__.ConfirmationInstructions

  def confirmation_instructions(user, confirmation_url) do
    # You'll want to update the User schema and the DB migration to support
    # storing first+last names, plan tiers, and whatever else you want to
    # associate to the user registration.
    rendered_email =
      ConfirmationInstructions.render(
        first_name: "Agent",
        last_name: "Smith",
        confirmation_url: confirmation_url,
        tier: "Gold"
      )

    new()
    |> to(user.email)
    |> from({"Onboarding Team", "welcome@saas-central.com"})
    |> subject("Welcome to SaaS-Central!")
    |> html_body(rendered_email)
    |> text_body("""
    Hello Agent Smith and thank you for signing up for the Gold plan!!!

    You can confirm your account by visiting the following URL: #{confirmation_url}
    """)
    |> deliver()
  end

  def generate_template(file_path) do
    {:ok, template} =
      file_path
      |> File.read!()
      |> Mjml.to_html()

    ~r/{{\s*([^}^\s]+)\s*}}/
    |> Regex.replace(template, fn _, variable_name ->
      "<%= @#{variable_name} %>"
    end)
  end
end

You’ll notice that we have hard coded a couple of things for our EEx template, mostly because I want to keep this tutorial laser focused and not jump around between lots of files. But if you do want to update the Phx.Gen.Auth generated files to support those additional fields, feel free to do so! We also have a function generate_template/2 that will take a file path, compile the MJML template using mjml_nif, and will then do some last mile tweaking of the template. The Rust MJML parser for whatever reason has issues parsing the file when the EEx template construct <%= %> is present in the template. As a result, we’ll be using the handlebars syntax {{ }} to denote that something references an assigns variable, and then through some excellently crafted RegEx, we’ll replace everything that looks like {{ first_name }} with <%= @first_name %>. Hopefully as the Rust mrml project matures, this will no longer be necessary.

Next, we’ll need to update our config/dev.exs file to ensure that our Swoosh module is configured to work with the local testing adapter as opposed to a real SaaS solution like SendGrid or Mandrill. Add the following entry to your dev.exs file:

config :mjml_demo, MjmlDemo.Emails, adapter: Swoosh.Adapters.Local

And also add the following to your lib/mjml_demo_web/router.ex file so that the mailbox can be accessed during development and only during development. When your project is compiled with MIX_ENV=prod this route will not be accessible.

if Mix.env() == :dev do
  scope "/dev" do
    pipe_through [:browser]

    forward "/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox"
  end
end

Step 4: Compiling our MJML templates and notifying the user - commit

At the top of the MjmlDemo.Emails module you’ll notice see that we make use of the ConfirmationInstructions module. Let’s add that module at lib/mjml_demo/emails/confirmation_instructions.ex with the following contents:

defmodule MjmlDemo.Emails.ConfirmationInstructions do
  @template_path Path.join([__DIR__, "confirmation_instructions.mjml"])
  @external_resource @template_path

  require EEx

  alias MjmlDemo.Emails

  rendered_mjml = Emails.generate_template(@template_path)
  EEx.function_from_string(:def, :render, rendered_mjml, [:assigns])
end

Let’s take some time to unpack this as this is where all the magic happens! The first attribute that we define, @template_path, states that our MJML template will reside adjacent to this module (thanks to the __DIR__ special form function). Next, we mark the module as having an external resource dependency on that file. This hints to the Elixir compiler that this module needs to be recompiled if the associated MJML template ever changes (huge thanks to @sorentwo for pointing me in the right direction on this one). This allows us to change our MJML template without worrying about having to manually recompile or update everything. No external tooling required, it’s all part of the Elixir compilation process now! Lastly, we leverage our generate_template/1 helper function to read the file, convert our handlebars syntax to EEx, and then using the function_from_string/4 macro, we generate a render/1 function in the MjmlDemo.Emails.ConfirmationInstructions that we will call in order to generate our email. Hopefully that all made sense…if not leave some comments down below!

With that in place, let’s create an MJML template at lib/mjml_demo/emails/confirmation_instructions.mjml with the following contents:

<mjml>
  <mj-body background-color="#ffffff">
    <mj-section background-color="#ffffff" padding-bottom="0px" padding-top="0">
      <mj-column vertical-align="top" width="100%">
        <mj-image src="https://images.unsplash.com/photo-1527247043589-98e6ac08f56c?width=600&crop=fit" align="center" border="none" width="600px" padding-left="0px" padding-right="0px" padding-bottom="0px" padding-top="0"></mj-image>
      </mj-column>
    </mj-section>
    <mj-section background-color="#009FE3" padding-bottom="0px" padding-top="0">
      <mj-column vertical-align="top" width="100%">
        <mj-text align="left" color="#ffffff" font-size="36px" font-weight="bold" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px" padding-bottom="10px" padding-top="50px">Welcome to SaaS-Central!</mj-text>
      </mj-column>
    </mj-section>
    <mj-section background-color="#009fe3" padding-bottom="20px" padding-top="20px">
      <mj-column vertical-align="middle" width="100%">
        <mj-text align="left" color="#ffffff" font-size="22px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px">
          <span style="color:#FEEB35">Dear {{ first_name }} {{ last_name }}</span>
        </mj-text>

        <mj-text align="left" color="#ffffff" font-size="20px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px" padding-top="20px">
          Thank you for signing up for a {{ tier }} plan! We're glad to have you onboard. Please click the button below to confirm your account!
        </mj-text>
        <mj-button align="center" font-size="22px" padding-top="25px" href="{{ confirmation_url }}" font-weight="bold" background-color="#ffffff" border-radius="10px" color="#1AA0E1" font-family="open Sans Helvetica, Arial, sans-serif">
          Confirm Account
        </mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

I won’t dive too much into this template and the MJML syntax, but do do checkout the great MJML documentation to see how you can create templates that meet your needs https://mjml.io/documentation/.

That last thing we need to do is to update our Phx.Gen.Auth code to leverage our new template. Luckily, Phx.Gen.Auth provides the UserNotifier module found at lib/mjml_demo/accounts/user_notifier.ex which gives us enough of an unopinionated skeleton to insert whatever suites our needs. To that end, let’s update the deliver_confirmation_instructions/2 function to use our MjmlDemo.Emails.ConfirmationInstructions module that we just put together:

def deliver_confirmation_instructions(user, url) do
  MjmlDemo.Emails.confirmation_instructions(user, url)
end

Step 5: Testing our user registration flow

With all that done, let’s start up our Phoenix application and give this all a spin! Start the Phoenix application by running mix phx.server and navigate to http://localhost:4000/users/register.

Once there, create a new account and then navigate to http://localhost:4000/dev/mailbox/ to see your rendered email template!

Finally, you can click on the Confirm Account button in the email and complete the Phx.Gen.Auth flow for confirming the user :).

Closing thoughts

Well done and thanks for sticking with me to the end! We covered quite a lot of ground and hopefully you picked up a couple of cool tips and tricks along the way. To recap, We set up a relatively simple flow of compiling MJML templates into EEx templates at compile time, and hooked up Swoosh with Phx.Gen.Auth to deliver confirmation emails to our users when they sign up.

Lastly, I want to thank @paulgoetze for his work on https://github.com/adoptoposs/mjml_nif as it has fixed a huge pain point for me and for that I am thankful!

Feel free to leave comments or feedback or even what you would like to see in the next tutorial. Till next time!


comments powered by Disqus