Sending emails with templates using MJML
Wednesday, October 23, 2019
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;">
</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
})
})