Automating HTML Emails with Metalsmith

Any front-end web developer who needs to build an HTML email has to revert back to the bad old days of table based layouts, spacer gifs and difficult cross-browser quirks that we thought were long in the past. Writing this kind of HTML is probably foreign to younger devs and brings back bad memories to those of us who had to navigate endless nested <tr><td> table rows and columns.

Writing the HTML for emails is error prone, painful and frustrating. Let a static site generator save you time and prevent mistakes.

The messy parts

The wide range of email platforms far exceeds the list of browsers we would typically test for, and includes arcane rules and inexplicable proprietary standards. The only way to know your email will look readable is to test it on all those platforms. It will certainly take some trial and error as you try to retain the original appearance in old clients like Outlook 2003.

As an example, a simple link meant to look like a button:

example button

Could be written like this:

<a href="http://example.com" style="display:inline-block;padding:12px 18px;background:#23408f;color:#fff;">Click here</a>

However in some email clients, such as Outlook, the padding will get stripped away and you're left with this:

example button

Getting a simple button to work should be one line of HTML and a few lines of CSS, but it often requires an entire <table> with lots of inline styles. There are a few options for creating this kind of style, but none of them are good. You'll be shocked by these 5 Weird tricks to make a button in HTML emails. You either allow your design to have its styles stripped for many of your recipients, or you resort to overly verbose chunks of HTML.

<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr>
    <td>
      <table border="0" cellspacing="0" cellpadding="0" align="center">
        <tr>
          <td align="center" bgcolor="#23408f">
          <a href="http://example.com" style="display: inline-block;color: #ffffff;padding: 12px 18px;">Click Here</a>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>

For a single button this might not be too bad, but making sure you get all your inline styles right and then copying and pasting that markup into other nested tables can become a maintenance nightmare. Hope you didn’t accidentally grab part of the next row! Even just finding the part of the HTML you want to edit can be difficult and get increasingly complicated as your layout grows.

Automation to the rescue

This is where a static site generator can help. Depending on your language of choice you may prefer Jekyll or Hugo. I'm more familiar with node so I'll setup Metalsmith.

Create a project folder, then install metalsmith, metalsmith-layouts and handlebars.

cd <your project folder>
npm init
npm i metalsmith metalsmith-layouts handlebars --save

Create a js file for the build config, call it index.js and add

var Metalsmith = require('metalsmith');
var layouts = require('metalsmith-layouts');
 
Metalsmith(__dirname)
  .source("src")
  .destination("build")
  .use(layouts({
    engine: "handlebars",
    partials: "layouts/partials"
  }))
  .build(function(err) {
  if (err) console.log(err);
  });

This includes metalsmith, metalsmith layouts, tells it to look in a src folder, output to a build folder and use handlebars as the template language. Create a src folder and a layouts folder which is where metalsmith-layouts expects to find the handlebars templates you'll make.

Test that this is working by creating a test.htm file in the src folder. Type anything in this file, then run node index.js to create a file at build/test.htm

Not too impressive so far, if everything went correctly you will just have an exact copy of your src/test.htm in the build folder.

Create a layout file called main.htm in layouts, add the following:

<h1>{{ title }}</h1>

Then in src/test.htm Write some yml instead of whatever text you put before.

---
title: New Email
layout: main.htm
---

The 3 dashes indicate to metalsmith that this is YAML, and you are setting values to a property. This is referred to as YAML “front-matter”. Setting the layout template will then pass "New Email" as the value of title. Run node index.js again to see the output, it should give you a h1 with the text "New Email".

If I wanted to make some buttons now, I could write them as YAML, all I need is the text and the url to make this link. In src/test.htm add this right after the title on its on line:

myLink:
  title: Button 1
  url: http://example.com/page1

Make a partial in layouts/partials/ called button.htm and paste in the table from before. Replace the href with {{ url }} and the "Click here" text with {{ title }}.

<table width="100%" border="0" cellspacing="0" cellpadding="0">
  <tr>
    <td>
      <table border="0" cellspacing="0" cellpadding="0" align="center">
        <tr>
          <td align="center" bgcolor="#23408f">
          <a href="{{url}}" style="display: inline-block;color: #ffffff;padding: 12px 18px;">{{title}}</a>
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>

Back in the layout template main.htm include the button partial, and pass the new data to it.

{{> button myLink }}

The properties of myLink (title, url) get passed into this "partial" template, then it in turn is included in your main template. To make an additional link, just add a new YAML property with its own title and url, then include it in the template.

The clarity of including just your data in the YAML file and not touching the HTML templates after they've been created will save some time, but it also helps with repetition and preventing errors. In a more complex example, I have several teasers with images, titles and links to "read more". The image and "read more" links should point to the same place, and the alt text on the image should be the same as the title.

In the YAML, we'll need a property with a title, url, and an image. There are 4 of them in total, so it makes sense to make a property like "teasers".

teasers:
  - title: You won't believe what happens next
    img: http://example.com/img/1.png
    url: htttp://example.com/bait1
  - title: 5 things you've done wrong your whole life
    img: http://example.com/img/2.png
    url: htttp://example.com/bait2
  - title: The reason why will shock you!
    img: http://example.com/img/3.png
    url: htttp://example.com/bait3
  - title: First you'll be shocked, then you'll be inspired
    img: http://example.com/img/4.png
    url: htttp://example.com/bait4

This is fairly well organized, and adding another one or fixing a url would be straightforward.

In the template main.htm add a new table, and write an "each" style include.

<table cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" width="200" align="center">
<tr>
{{#each teasers}}
{{>teaser}}
{{/each}}
</tr>
</table>

This is looping over each item in your "teasers" property and passing their values to the "teaser" partial. The naming convention you use doesn't matter, as long as the properties like title, img, url are the same in your YAML as they are in your templates.

Create a partial at layouts/partials/teaser.htm.

<td align="center">
  <a href="{{url}}"><img src="{{img}}" alt="{{title}}" style="display:block;" /></a>
  <h2>{{title}}</h2>
  <p><a href="{{url}}">read more...</a></p>
</td>

Here the url gets used in both the image and the read more link. The title gets used in the h2 and as the image alt text. Run node index.js to build the HTML file.

These are very simple examples, but they should illustrate how a little setup can save you time and help prevent errors and typos. Using a static site generator, like Metalsmith, lets you reuse snippets of HTML and makes propagating fixes across your whole file quickly. This strategy is especially useful if you have something like a monthly newsletter. Layouts can be saved, swapped out, updated and re-used very easily and your template will grow in usefulness as you add more options and partials. Automating the build and separating out tested snippets of HTML not only improves the speed of each subsequent email based on this template, but it keeps you from accidentally introducing fresh mistakes into previously tested sections.

Code JavaScript Process

Read This Next