DEV Community

Cover image for Sending emails with templates using MJML
Clay Murray
Clay Murray

Posted on

Sending emails with templates using MJML

Sending emails is something a lot of web apps like to do. Password resets, notifications, promotions etc.

One of the biggest annoyances in sending emails is HTML EMAILS! They are very messy, ugly, and impossible to figure out.

In the past we would design our emails using Mailchimp then export them as an HTML email. This results in a convoluted mess of HTML that no one wants to make minor edits to.

Can we do better? Of course we can that's why I wrote this article!

MJML

Enter MJML. It's a neat little library to make it easier to keep your HTML emails as code without going crazy!

This is just a quick example from their site:

<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-image width="100px" src="https://mjml.io/assets/img/logo-small.png"></mj-image>
        <mj-divider border-color="#F45E43"></mj-divider>
        <mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

As you can see, it is very readable and HTML like. Much easier to edit and maintain!

They even have a free online editor to see what your email will look like!

MJML will take the code that you write and transform it into something like this:

<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">

<head>
  <title> </title>
  <!--[if !mso]><!-- -->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <!--<![endif]-->
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #outlook a {
      padding: 0;
    }

    .ReadMsgBody {
      width: 100%;
    }

    .ExternalClass {
      width: 100%;
    }

    .ExternalClass * {
      line-height: 100%;
    }

    body {
      margin: 0;
      padding: 0;
      -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
    }

    table,
    td {
      border-collapse: collapse;
      mso-table-lspace: 0pt;
      mso-table-rspace: 0pt;
    }

    img {
      border: 0;
      height: auto;
      line-height: 100%;
      outline: none;
      text-decoration: none;
      -ms-interpolation-mode: bicubic;
    }

    p {
      display: block;
      margin: 13px 0;
    }
  </style>
  <!--[if !mso]><!-->
  <style type="text/css">
    @media only screen and (max-width:480px) {
      @-ms-viewport {
        width: 320px;
      }
      @viewport {
        width: 320px;
      }
    }
  </style>
  <!--<![endif]-->
  <!--[if mso]>
        <xml>
        <o:OfficeDocumentSettings>
          <o:AllowPNG/>
          <o:PixelsPerInch>96</o:PixelsPerInch>
        </o:OfficeDocumentSettings>
        </xml>
        <![endif]-->
  <!--[if lte mso 11]>
        <style type="text/css">
          .outlook-group-fix { width:100% !important; }
        </style>
        <![endif]-->
  <style type="text/css">
    @media only screen and (min-width:480px) {
      .mj-column-per-100 {
        width: 100% !important;
        max-width: 100%;
      }
    }
  </style>
  <style type="text/css">
    @media only screen and (max-width:480px) {
      table.full-width-mobile {
        width: 100% !important;
      }
      td.full-width-mobile {
        width: auto !important;
      }
    }
  </style>
</head>

<body>
  <div style="">
    <!--[if mso | IE]>
      <table
         align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
      >
        <tr>
          <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
      <![endif]-->
    <div style="Margin:0px auto;max-width:600px;">
      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
        <tbody>
          <tr>
            <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
              <!--[if mso | IE]>
                  <table role="presentation" border="0" cellpadding="0" cellspacing="0">

        <tr>

            <td
               class="" style="vertical-align:top;width:600px;"
            >
          <![endif]-->
              <div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
                <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
                  <tr>
                    <td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
                      <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
                        <tbody>
                          <tr>
                            <td style="width:100px;"> <img height="auto" src="https://mjml.io/assets/img/logo-small.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;" width="100" /> </td>
                          </tr>
                        </tbody>
                      </table>
                    </td>
                  </tr>
                  <tr>
                    <td style="font-size:0px;padding:10px 25px;word-break:break-word;">
                      <p style="border-top:solid 4px #F45E43;font-size:1;margin:0px auto;width:100%;"> </p>
                      <!--[if mso | IE]>
        <table
           align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #F45E43;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px"
        >
          <tr>
            <td style="height:0;line-height:0;">
              &nbsp;
            </td>
          </tr>
        </table>
      <![endif]-->
                    </td>
                  </tr>
                  <tr>
                    <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
                      <div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;"> Hello World </div>
                    </td>
                  </tr>
                </table>
              </div>
              <!--[if mso | IE]>
            </td>

        </tr>

                  </table>
                <![endif]-->
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <!--[if mso | IE]>
          </td>
        </tr>
      </table>
      <![endif]-->
  </div>
</body>

</html>

AHHHHH!!!! That's just terrifying. We thank MJML for fighting the demons for us!

Now we can transform our MJML to HTML but it's currently static.

So what about templating you may ask?

Well...

Templating

We still probably want to be able to use our MJML to make email templates. We want nice things like our user's name and custom links. Good web stuff.

For that I use mustache. It's fairly simple to use:

Our template:

<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-image width="100px" src="https://mjml.io/assets/img/logo-small.png"></mj-image>
        <mj-divider border-color="#F45E43"></mj-divider>
        <mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello {{user}}</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Our code:


const mustache = require("mustache");

const templateData = {
    "user": "John"
}

const renderedMJML = mustache.render(mjmlTemplate, templateData);

Now we have filled in our template with mustache. But we are still in the MJML format.

Why did we do that? Well MJML makes huge transformations to the code we hand to it. If we try to run mustache after we convert to HTML we will lose our ability to use mustache.

Luckily mustache doesn't much care what type of document we throw at it. It only cares about {{}}. (Incidentally this allows you to use mustache in many other applications including JSON)

Let's now convert from MJML to HTML.

const mjml = require("mjml");
const html =  mjml(renderedMJML).html;
// don't forget the `.html`

Now we have some HTML but we still need to...

Sending email

Okay we now have an HTML template. We want to send it.
I am going to use the Postmark Api because it's really easy.

const fetch = require("node-fetch");

await fetch("https://api.postmarkapp.com/email", {
    method: "POST",
    headers: {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "X-Postmark-Server-Token": "server token"
    },
    body: JSON.stringify({
        To: "example@example.com",
        From: "example2@example.com",
        Subject: "This is a test",
        HtmlBody: html
    })
})

There you go.

Full Javascript

const fetch = require("node-fetch");
const mustache = require("mustache");
const mjml = require("mjml");

const templateData = {
    "user": "John"
}

const mjmlTemplate = `
<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-image width="100px" src="https://mjml.io/assets/img/logo-small.png"></mj-image>
        <mj-divider border-color="#F45E43"></mj-divider>
        <mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello {{user}}</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>
`

const renderedMJML = mustache.render(mjmlTemplate, templateData);

const html =  mjml(renderedMJML).html;
// don't forget the `.html`


await fetch("https://api.postmarkapp.com/email", {
    method: "POST",
    headers: {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "X-Postmark-Server-Token": "server token"
    },
    body: JSON.stringify({
        To: "example@example.com",
        From: "example2@example.com",
        Subject: "This is a test",
        HtmlBody: html
    })
})

Top comments (4)

Collapse
 
edgarascom profile image
Edgaras Benediktavicius

Is there a way to import template from another file?

Collapse
 
yourdevguy profile image
Mike Haslam

Thanks for the post. Can you give more info on your app or a repo link

Collapse
 
popenke profile image
Lucas Popenke

thank you! your post helped me a ton today.

Collapse
 
powerc9000 profile image
Clay Murray

Sweet! Glad it helped.