< Back

March 1st, 2024

How we Simplified Our Email Design Workflow With MJML.

{ Engineering }

How we Simplified Our Email Design Workflow With MJML.


Cowrywise sends many transactional emails, a key component of customer interactions on our app. We send a ton of emails, which include welcome emails (after signing up to inform customers of the next steps), success emails (after investing in a mutual fund), match-completed emails (after saving through Cowrywise Triggers), etc.

Currently, we send about 190 different notifications over multiple channels, including push, in-app, and email notifications. Email notifications account for around 37% of the total sent. It’s a considerable chunk. With this volume of email notifications, we needed a system that lets us quickly design and implement email templates while ensuring they appear uniformly across different email clients.

Until now, we’ve designed our templates using the classic HTML tables. Anyone who has created email templates with HTML tables can confirm how frustrating and unpleasant it can be. Besides being challenging to design, guaranteeing consistency across email clients was very tricky.

MJML To The Rescue

In one of our engineering talks, Feyi, our lead designer, introduced the team to MJML, which has a neat syntax and good documentation. It promises faster email development and, more importantly, guarantees a consistent look across different email clients.

For context, this is what a sample button written with HTML tables and MJML looks like.

The button.

MJML Source

<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-button background-color="#0067F5" href="https://cowrywise.com">Click Me!</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

HTML Source

<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">

<head>
  <title>
  </title>
  <!--[if !mso]><!-->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <!--<![endif]-->
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #outlook a {
      padding: 0;
    }

    body {
      margin: 0;
      padding: 0;
      -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
    }

    table,
    td {
      border-collapse: collapse;
      mso-table-lspace: 0pt;
      mso-table-rspace: 0pt;
    }

    img {
      border: 0;
      height: auto;
      line-height: 100%;
      outline: none;
      text-decoration: none;
      -ms-interpolation-mode: bicubic;
    }

    p {
      display: block;
      margin: 13px 0;
    }
  </style>
  <!--[if mso]>
        <noscript>
        <xml>
        <o:OfficeDocumentSettings>
          <o:AllowPNG/>
          <o:PixelsPerInch>96</o:PixelsPerInch>
        </o:OfficeDocumentSettings>
        </xml>
        </noscript>
        <![endif]-->
  <!--[if lte mso 11]>
        <style type="text/css">
          .mj-outlook-group-fix { width:100% !important; }
        </style>
        <![endif]-->
  <!--[if !mso]><!-->
  <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
  <style type="text/css">
    @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
  </style>
  <!--<![endif]-->
  <style type="text/css">
    @media only screen and (min-width:480px) {
      .mj-column-per-100 {
        width: 100% !important;
        max-width: 100%;
      }
    }
  </style>
  <style media="screen and (min-width:480px)">
    .moz-text-html .mj-column-per-100 {
      width: 100% !important;
      max-width: 100%;
    }
  </style>
  <style type="text/css">
  </style>
</head>

<body style="word-spacing:normal;">
  <div style="">
    <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
    <div style="margin:0px auto;max-width:600px;">
      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
        <tbody>
          <tr>
            <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
              <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
              <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
                <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
                  <tbody>
                    <tr>
                      <td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
                        <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
                          <tr>
                            <td align="center" bgcolor="#0067F5" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#0067F5;" valign="middle">
                              <a href="https://cowrywise.com" style="display:inline-block;background:#0067F5;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Click Me! </a>
                            </td>
                          </tr>
                        </table>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
              <!--[if mso | IE]></td></tr></table><![endif]-->
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <!--[if mso | IE]></td></tr></table><![endif]-->
  </div>
</body>

</html>

A Missing Piece

We found a solution that helps us write templates faster. More people can contribute, and consistency is guaranteed. However, we discovered an issue. Something was missing. We couldn’t use MJML directly; it had to be converted into HTML files first.

Our next problem: how can we efficiently and quickly convert MJML templates to HTML files?

Here’s some context on how we send notifications.

We have a dedicated notification service, and we define templates for each type of notification per channel (push, in-app, or email). So, for example, if we want to send a successful top-up notification by email, we first need to create the template on the notification service, which includes fields such as the notification key, a link to the HTML template, and an email subject. The HTML file is always hosted on GitHub.

The First Solution

The first solution we tried was to convert the MJML file on the fly. It involved using the link for the MJML file to set up the email. So, the notification service would fetch the source file and use the MJML compilation AP(conversion API) to convert it to HTML before generating the email’s context and sending it to customers.

This solution had two setbacks. One was the expense of converting the MJML templates on the fly, which would quickly add up every time we sent notification emails. The second issue was generating MJML files that contained partials (Partials are reusable code segments in the MJML file). We did not convert the partials with the template since we relatively imported them into the MJML file.

It was quickly evident that this solution was not going to work.

The Second Solution

The second solution we tried was pre-compiling the MJML templates into HTML. After that, we’d use the resulting HTML files from the conversion in the notification service. We did this pre-compilation by writing a build script that steps through all the MJML files in a source directory and exports the converted version to a destination directory. We integrated it into a GitHub Action workflow that runs on every pull request. This solution was better for us.

The Build Script

#!/bin/bash

source_folder="src/templates"
dist_folder="dist"

echo "Source folder is $source_folder"
echo "Dist folder is $dist_folder"

find "$source_folder" -type f -name "*.mjml" | while read -r file; do
  if [[ -f "$file" ]]; then
    # Get the relative path of the file within the source folder.
    relative_path="${file#$source_folder}"

    # Remove the leading slash if present from the relative path, if present.
    relative_path="${relative_path#/}"

    # Build the output path in the dist folder.
    # Also, replace the .mjml extension with .html
    dist_path="$dist_folder/${relative_path%.mjml}.html"

    echo "Dist path is $dist_path"

    # Create the directory structure in the dist folder if it doesn't exist.
    mkdir -p "$(dirname "$dist_path")"

    # Run the validation command for the current file.
    mjml -v "$file"

    # Run the build command for the current file.
    mjml -l strict -r "$file" --config.minify true -o "$dist_path"

    echo "Built $file"
  fi
done

The build script converts MJML templates into HTML files.

We specified two directories in the script: the source directory (src/templates) for the MJML files and a destination directory (dist) for the converted HTML files. The script iterates through all the MJML files in the source folder, validates them, and exports the resulting HTML files to the dist directory.

The GitHub CI Workflow

name: Build MJML

on:
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v2
      with:
        fetch-depth: 0

    - name: Use Node.js 18.x
      uses: actions/setup-node@v2
      with:
        node-version: 18.x

    - name: Install Dependencies
      run: npm install -g mjml@4.14.1

    - name: Run Compilation Script
      run: |
        chmod +x build_mjml.sh
        ./build_mjml.sh

    - name: Check for Changes
      id: git-diff-check
      run: |
        if [[ -n "$(git status --porcelain)" ]]; then
          echo "::set-output name=changed::true"
        fi

    - name: Commit Files and Push Changes
      if: steps.git-diff-check.outputs.changed == 'true'
      run: |
        git config --local user.email "github-actions[bot]@users.noreply.github.com"
        git config --local user.name "github-actions[bot]"
        
        git add .
        git commit -m "Compile MJML files to HTML"
        git push origin HEAD:${{ github.head_ref }}

This GitHub Action job is triggered on every pull request targeting the master branch. The steps within the job include:

  • Checking out the repository.
  • Setting up Node.js 18.X.
  • Installing the MJML dependency.
  • Executing the compilation script (build_mjml.sh).

We gave the script execution permissions to convert the MJML templates to HTML.

Following the conversion, the action checks for changes in the repository using a Git diff check. If changes are detected, the action pushes the changes back to the original branch of the pull request, ensuring that the converted HTML files are updated as part of the pull request.

With this in place, anytime we create a PR, the GitHub Actions workflow builds and converts the necessary MJML files and then pushes them back to the PR. All we now have to do is include a link to the email template available on GitHub in the notification template for the notification service. The notification service then processes it without needing to know the language with which we created the template.

Goal Achieved

This workflow has considerably eased things. Now, we can quickly create new email templates that are easy to maintain. Once we push the templates to the GitHub repository, there’s a standing process to convert them into HTML so that the notification service can use them. We’ve successfully switched from writing time-consuming HTML files to writing clean, visually consistent, easy-to-maintain MJML files.

We hope you gained some insights into simplifying your email template design workflow.