Rapid Development Email Templates with Node.js
Contents
Sending Automated Emails in Node.js
When I was tasked with solving the age-old problem of sending automatic email messages to our users at Indaba Music, I surveyed the Node.js landscape to find out the state of affairs.
I was pleased to immediately discover node-email-templates and Nodemailer, and within two days had a proof of concept email notification server deployed to production.
node-email-templates helps organize your project and make it easy to render templates for sending via email by using juice to inline css.
Nodemailer does the actual email dispatching - given an email dispatch service, a subject, to, and body, Nodemailer will get your mail to its destination.
Nodemailer is a wonderful piece of software. It worked, and continues to work, exactly as advertised and without any hiccups. It even has a convenient API that makes integration with common email dispatchers, such as SendGrid (which I also recommend), quite painless.
Unfortunately this journey was not without a few obstacles that node-email-templates provided for me to solve.
What follows is an explanation of the bumps along the road that caused me to write these modules to improve the state of email templates in node.js:
More on these in a bit.
node-email-templates Gotchas
Assume that I have 2 templates named reminder
and notice
.
node-email-templates requires your project to have a folder structure that looks like this:
./templates/reminder/html.ejs ./templates/reminder/text.ejs ./templates/reminder/style.css ./templates/notice/html.ejs ./templates/notice/text.ejs ./templates/notice/style.css
There are several problems here. There are some module smells:
-
Regardless of whether or not you need text version of a template, you must have the
text.ejs
file there or node-email-templates will throw an error. - This project is poorly maintained. As of this writing, daeq submitted a pull request to fix the above problem 3 months ago, and despite a promise to merge it, niftylettuce still has not done so.
But there are also some fundamental problems with the approach that the module takes.
Fundamental Flaws
- This flavor of ejs is limited. You can do includes, but not layouts or template inheritance, which is where the true value of using templates comes in.
- The html templates have no way of sharing css between them.
-
Because ejs depends on using
eval
, it is impossible to, given a template, create a dummy context with which to generate a preview of the template. More on this later.
Includes VS Template Inheritance
To demonstrate the template inheritance problem, let me give you 2 versions of a template, one using ejs with includes, and one using swig with template inheritance:
ejs includes
notice.ejs
<% include header %>
<div>
<p>Hey <%= username %>,</p>
<p>This is a notice that your offer is about to expire.</p>
</div>
<% include footer %>
reminder.ejs
<% include header %>
<div>
<p>Hey <%= username %>,</p>
<p>Don't forget! You probably wanted to do that thing.</p>
</div>
<% include footer %>
header.ejs
<div>
<img src="logo.png">
</div>
footer.ejs
<div>
Super Cool & Co., LLC.
<a>Privacy Policy</a>
</div>
swig template inheritance
reminder.html
{% extends "base.html" %}
{% block content %}
<p>Don't forget! You probably wanted to do that thing.</p>
{% endblock %}
notice.html
{% extends "base.html" %}
{% block content %}
<p>This is a notice that your offer is about to expire.</p>
{% endblock %}
base.html
<div>
<img src="logo.png">
</div>
<div>
<p>Hey {{ username }},</p>
{% block content %}
{% endblock %}
</div>
<div>
Super Cool & Co., LLC.
<a>Privacy Policy</a>
</div>
Template Inheritance Wins
This is an oversimplified example, but even so it starts to become obvious why template inheritance is superior to includes.
You can also have includes in swig, by the way.
Sharing CSS
node-email-templates uses juice to inline css. Give juice html and css, and it returns html with the css inlined on each element for maximum email client compatibility.
This setup seems good at first, but it is crippled by the fact that templates
are completely unable to share css.
Each template has its own independent style.css
file.
It is not node-email-templates's fault. Given the way that juice works, it isn't really possible to share css.
This is where boost comes in. boost depends on juice and adds 2 key features that make sharing CSS possible.
- Ability for the html to have
<link rel="stylesheet">
tags which are resolved correctly and have the resulting CSS applied. - Ability to have
<style>...</style>
elements and have that CSS applied as well.
When you add this capability with template inheritance, sharing CSS becomes a solved problem. For example:
base.html
<html>
<head>
{% block css %}
<link rel="stylesheet" href="base.css">
{% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
reminder.html
{% extends "base.html" %}
{% block css %}
{% parent %}
<link rel="stylesheet" href="reminder.css">
{% endblock %}
{% block content %}
<h1>Reminder</h1>
<p>Don't forget!</p>
{% endblock %}
notice.html
{% extends "base.html" %}
{% block css %}
{% parent %}
<link rel="stylesheet" href="notice.css">
{% endblock %}
{% block content %}
<h1>Notice</h1>
<p>This is a notice.</p>
{% endblock %}
Dummy Context
In order to rapidly build email templates, we need to see what we are building as we build it.
Because you need to supply a template with a context in order to render it and see it, this makes seeing what you are doing while you build templates a two step process.
We can remove this extra step by taking advantage of swig-dummy-context, a module I wrote which, given a swig template, gives you a "dummy" context - a fill-in-the-blank structure you can use to immediately preview your template.
Given:
<div>
{{ description }}
</div>
{% if articles %}
<ul>
{% for article in articles %}
<li>{{ article.name }}</li>
{% endfor %}
</ul>
{% else %}
<p>{{ defaultText }}</p>
{% endif %}
swig-dummy-context produces:
{
"description": "description",
"articles": {
"name": "name"
},
"defaultText": "defaultText"
}
And if you render the template with the generated dummy context, you get:
<div>
description
</div>
<ul>
<li>name</li>
</ul>
Conclusion
swig-email-templates gives you all the ingredients you need to build well-organized templates and gives you the tooling that you need to build a live preview tool.
At Indaba Music we have such a tool. It lets you select a template from the templates folder and fill in the substitutions to preview how an email will look. To be extra sure of how an email will render, you can use the tool to send a test email to your email address.
This tool is currently private as it is not decoupled from SendGrid or even polished up for 3rd party use at all; however if there is sufficient interest I may open source it.