Creating custom emails with HTML, plus gotchas!

Dec 9, 2022

Recently I've been learning quite a bit about how to create custom generated emails. Here's a few notes on what I learned so far.

Building HTML emails sucks

First of all it's important to understand that coding for email clients is sort of like coding for Internet Explorer... but make it 10x worse.

If you attempt to do it from scratch, basically prepare yourself to forget everything you think you know about HTML and CSS, 90% of it will not work the way that you expect it to work.

Email clients are basically like browsers stuck in the past with extremely limited supoprt for most HTML/CSS that you would consider "modern".

You'll find yourself using <table> for pretty much all of the layout and several other seemingly hacky things just to get things to look reasonable. And even then after testing you'll probably find out several email clients will completely struggle to render your email in a consistent manner.

So here's what I recommend you do instead.

Start with a template

Do you just need something basic and simple? Start with a template like this one. If you're just trying to get something out quickly, that's probably a good place to start.

Zurb also has a set of templates that has already been tested in several email clients.

Pick one, modify it a little bit, and run with it.

However the moment that you start needing to account for a specific design, you'll find that any template ultimately will be painful to upgrade to that design.

Becoming an email HTML expert is not worth your time, trust me.

Here's what you should do instead.

Pick an HTML framework

From my own research I found that there are pretty much two main players in the email framework space:

Both of these frameworks try to make the experience a little less painful, although they have slightly different philosophies and learning curves. For example (from my understanding), MJML tends to be mobile-first, simply allowing for content to stretch (or limited to a max width) on bigger screens with minimal layout shifts.

Foundation for Emails on the other hand tends to be more desktop-first, you can see that Media Queries are used to get a proper layout for mobile.

Both of them attempt to get you out of the "table soup" via some kind of light templating markup language.

Now to be honest I did not spend a ton of time comparing the two. I started with MJML and found it dead simple to drop into my project and workflow (more on that later).

Foundation for emails at quick glance looks like it basically requires setting up a mini project/server with its own folder structure and a bunch of boilerplate. That felt like a bit too much to start with for my use case.

MJML simply requires a single .mjml file to get started

That's right. One file!

You can even just code something up in their online editor just to try it out.

If you use VS Code, install the MJML extension. It's great.

You get syntax highlighting, formatting, linting (informs you when you're using the components incorrectly) and a live preview of the resulting HTML. No server needed.

The extension also allows you to copy or export the HTML output.

So basically you can drop this solution into any project with pretty much zero config. That's pretty incredible. It does come with a CLI that you can install if you want to automate outputting the HTML.

The documentation is quite simple and easy to follow, so I don't even feel the need to explain how MJML works. Read the docs. Learn about some of the built-in components, and you'll find yourself up and running in no time.

What about dynamic content?

Although MJML introduces a basic markup language, it's still not quite like your traditional templating language. It doesn't handle or solve templating at all. Although that's totally okay since there are already several templating solutions that exist like EJS and handlebars. You can use these solutions to allow for dynamic things like rendering a list from an array, conditionally rendering blocks, etc.

Mixing templating languages with MJML is fairly straightforward. Basically just write the templating syntax right in your MJML code. Then when you output the HTML save it as .hbs for example with handlebars or .ejs for EJS. Then just hand it off to your templating engine.

Note that in some situations with MJML you'll want to utilize the mj-raw component to wrap some of your templating syntax. For example if you were trying to conditionally render something with handlebars it might look something like this:

<mjml>
  <mj-body>
    <mj-raw> {{#if hasName}} </mj-raw>
    <mj-text>{{name}}</mj-text>
    <mj-raw> {{/if}} </mj-raw>
  </mj-body>
</mjml>

The {{name}} expression in the mj-text is fine, however the conditional helpers which wrap components often need to be within mj-raw so that MJML completely ignores it.

Okay but how do we then go from templating language to actual HTML?

Each of these templating solutions often come with their own compilers however since we know our target is sending emails I would recommend solutions that are already geared towards that.

For Node/Express apps I highly recommend checking out email-templates which is basically nodemailer under the hood but it adds built-in support for almost any templating engine plus packages preview-email to let you see your rendered (HTML) email on the browser.

Note though that preview-email is not a reliable way to test that your email will render correctly in email clients. Since that still opens the email on your browser.

For Nest.js apps you can try @nestjs-modules/mailer which is fundamentally the same idea as email-templates although it only supports pug, EJS, and handlebars.

For Node apps nodemailer is pretty much the only thing you need to actually send the emails. You can have it utilize an SMTP server or use other transports like Amazon SES.

There are still some gotchas to know about

Now all these tools that I mention definitely make writing HTML emails a little less painful. But there are still a handful of seemingly random gotchas that I personally had to still learn about.

Here's a short list of random ones that I can remember:

I'll probably add to this list as I learn more, but that's it for now!