Creating custom emails with HTML, plus gotchas!

Picture of MariusMarius Espejo

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:

    <mj-raw> {{#if hasName}} </mj-raw>
    <mj-raw> {{/if}} </mj-raw>

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:

  • Embedding an image is tricky.
    • You can try inlining (Base64) images, but it's generally a bad idea because most clients don't support it. I know Gmail for example blocks images like that by default. It can also add a lot to the size of your email.
    • You can try CID embedded images, which basically means you attach images as actual email file attachments and have your HTML reference the ID of that.
      • Tip: there's a nodemailer plugin that takes your inline Base64 images and automatically translates it to the CID-referenced equivalent. I have not personally tried it yet.
      • Note that this also affects the size of your email.
    • Potentially the most reliable way to support images is to host them externally, however some clients might also block that.
    • Basically if you can: minimize the amount of images for less headaches.
  • Gmail clips emails if it's too large in size and hides the full content behind a “View entire message” link.
    • This is also why you might want to avoid embedding inline images, it might make your email much more likely to be clipped.
    • If you have unsubscribe links in your footer and it is often hidden because of clipping, your emails might end up getting marked as spam more often. Not good.
    • Clipping can also prevent tracking code that might be in your HTML from rendering. Typically these would be invisible images at the very bottom of your content. That means you might get unreliable analytics (e.g. if you were tracking email opens).
  • Make sure to read about legal requirements (or consult actual legal advice)
    • For example from my understanding in the US it is actually legal to send unsolicited commercial emails to people, however you need to provide a way for recipients to opt-out. Note that this is not true for every country. Some require that you explicity get permission before sending an email.
  • You might find yourself attempting to write custom CSS to get the perfect layout, even while using one of the email frameworks mentioned. However I personally found through testing that everytime I went "out of the box" of the framework there's always at least one email client where it just will not work. For example Outlook doesn't understand padding. Yes you read that correctly.
    • Avoid too much custom CSS if you can. Keep it simple.
  • Consider using a testing tool like Litmus. You can send it a sample email and it will automatically generate previews of that email in several different clients.
    • Trying to manually test your emails across several clients and operating systems is probably hard for most people. For example you likely don't have both an iPhone and an Android, or both a PC and a Mac. It's also probably not easy to install older email clients, your OS might not allow it.
    • Alternatively, just keep your email design dead simple and you probably won't have to worry about testing as much. It's usually the fancy layouts, shapes, colors, and spacing that you'll need to watch out for in these tests.
  • Don't forget to account for Dark Mode!
    • Some clients will automatically invert whites, blacks, and some grays.
    • As for other colors, sometimes the client will automatically adjust the shade of that color. Other times the client will completely leave it as-is. I personally don't yet know the rules behind this. But as a rule of thumb: if you have text on a colored background, make sure that text is readable on that color whether it is black or white, for the cases where the email client doesn't auto adjust the background color but does invert the text color.
  • Don't mass send using the BCC field.
    • If you have an email that you want to send to a large group of people, you probably don't want to add all of them to the TO field, that would make recipients aware of who your other recipients are. You might consider using the BCC field instead, however I read that you're more likely to hit spam filters that way.
    • Basically if you want to be safe as much a possible you should send each email individually using one email on the TO field.

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

👈 More posts