Rapid Email

For anyone who's had to work on system-generated emails, updating them for even a minor change can be a pain, but it doesn't have to be anymore!

Wouldn't it be nice to have a version control system for your email code, and reusable components for repetitive parts, built specifically for developers in an enterprise (or small!) company?

Requirements

  • Reusable Components
  • Version Controlled
  • Ease-of-setup

Tools

  • NodeJS
  • Javascript

Step 1

Setup NodeJS and NPM.

NodeJS has incredible cross-platform support and can be installed with a simple download. After setup, I used NPM to install an open source HTML-generating package called 11ty.

Step 2

Build the UI.

The 11ty HTML framework allows you to create reusable components and use templating code much like the Twig template engine or Blade template engine for PHP. You can use it to create semantic templates so your IDE doesn't throw errors when you leave open tags for your header and footer.

You can also create dynamic templates that take in unique content like buttons and marketing banners. The below example shows three main folders: Layouts, Reusable Components, and the Email Content folder. This structure allows you to work on just the email content you need without having to worry about the header, footer, buttons, or anything else that is shared across multiple email templates.

Folder Structure

/RapidEmail/
├── layouts
│ ├── parts
│ │ ├── header.html
│ │ └── footer.html
│ │
│ ├── base.html
│ ├── marketing.html
│ ├── security.html
│ └── service.html

├── reusable-components
│ ├── advertisement.html
│ ├── button.html
│ └── promotion-banner.html

├── email-content
│ ├── 02-14-2021-valentines-day.html
│ ├── new-user-registration.html
│ └── 2021-new-site-features.html

NodeJS Template

<!doctype html>
<html>
<head>
{{ include "meta-tags" }}
<title>{{ title }}</title>
{{ include "global-styles" }}
</head>
<body class="">
<span class="preheader">{{ descriptionPreview }}.</span>
<table role="presentation" class="body">
<tr>
<td class="container">
<div class="content">

{{ include "header" }}

{{ content }}

{{ include "footer" }}

</div>
</td>
</tr>
</table>
</body>
</html>

Compiled HTML

<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Valentines Day 2021!</title>
<style>
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}

body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}

table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}

/* -------------------------------------
BODY & CONTAINER
------------------------------------- */


.body {
background-color: #f6f6f6;
width: 100%;
}

/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}

/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
}

/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */

.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}

.wrapper {
box-sizing: border-box;
padding: 20px;
}

.content-block {
padding-bottom: 10px;
padding-top: 10px;
}

.footer {
clear: both;
margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a
{
color: #999999;
font-size: 12px;
text-align: center;
}

/* -------------------------------------
TYPOGRAPHY
------------------------------------- */

h1,
h2,
h3,
h4
{
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}

h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}

p,
ul,
ol
{
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li
{
list-style-position: inside;
margin-left: 5px;
}

a {
color: #3498db;
text-decoration: underline;
}

/* -------------------------------------
BUTTONS
------------------------------------- */

.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}

.btn-primary table td {
background-color: #3498db;
}

.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}

/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */

.last {
margin-bottom: 0;
}

.first {
margin-top: 0;
}

.align-center {
text-align: center;
}

.align-right {
text-align: right;
}

.align-left {
text-align: left;
}

.clear {
clear: both;
}

.mt0 {
margin-top: 0;
}

.mb0 {
margin-bottom: 0;
}

.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}

.powered-by a {
text-decoration: none;
}

hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
margin: 20px 0;
}

/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */

@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a
{
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article
{
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}

/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */

@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div
{
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}

</style>
</head>
<body class="">
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td class="container">
<div class="content">

<!-- START HEADER -->
<div class="header">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<span class="apple-link">Our Logo</span>
</td>
</tr>
<tr>
<td class="content-block powered-by">
Powered by <a href="http://htmlemail.io">HTMLemail</a>.
</td>
</tr>
</table>
</div>
<!-- END HEADER -->

<table role="presentation" class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Hi there,</p>
<p>Sometimes you just want to send a simple HTML email with a simple design and clear call to action. This is it.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="http://htmlemail.io" target="_blank">Call To Action</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p>This is a really simple email template. Its sole purpose is to get the recipient to click the button with no distractions.</p>
<p>Good luck! Hope it works.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>

<!-- START FOOTER -->
<div class="footer">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<span class="apple-link">Company Inc, 3 Abbey Road, San Francisco CA 94102</span>
<br> Don't like these emails? <a href="http://i.imgur.com/CScmqnj.gif">Unsubscribe</a>.
</td>
</tr>
<tr>
<td class="content-block powered-by">
Powered by <a href="http://htmlemail.io">HTMLemail</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->

</div>
</td>
</tr>
</table>
</body>
</html>

Step 3

Create your custom components.

Email development can be complicated due to the low complexity support provided by email clients. Outlook Web, Gmail, Yahoo, and Apple Mail are all very modern email systems but a lot of people still used downloaded applications like Outlook. For this reason, its typically encouraged to use the old table format and inline all of your CSS to provide maximum compatibility across the different email apps.

NodeJS Component

<h1>Shop Now for Valentines Day</h1>

<p>Some really exciting copy here about why your product should be purchased this year on Valentine's Day. At this point, it doesn't even need to be romantic, as long as it sounds cool, right?!</p>

<!-- Component Name, URL, Button Text -->
{% buttonCTA "/promotion-url/", "Click Here To Shop!" %}

Compiled HTML

<h1>Shop Now for Valentines Day</h1>

<p>Some really exciting copy here about why your product should be purchased this year on Valentine's Day. At this point, it doesn't even need to be romantic, as long as it sounds cool, right?!</p>

<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
<a href="/promotion-url/" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Click Here To Shop!</a>.
</td>
</tr>
</table>

Step 4

Import into your tool of choice.

NodeJS (or deployment pipeline) will output your compiled HTML into a _site folder where you can grab the HTML and upload it to whatever tool you want to use.

One of my favorite projects was building this into an Azure DevOps pipeline and integrating it into Azure Blob Storage so that our Azure email system can read the values in a key/value format. Each email is setup to look for a specific key and that corresponding value contains all the HTML content, along with any custom Azure variables we want to include.

Step 5

Time to Relax

Your emails are now organized, templated, and version controlled in whichever versioning system you want to use. Time to sit back and never have to worry about email development again.

Minutes may seem as worthless as pennies until you add them up. Neither should be wasted.

Finito! (The End)

Thanks for reading!