Back Arrow
From the blog

Setting Up a Frontend Build for HTML Email Templating with MJML

In this article, we'll break down two key stages: first, we'll create a repository for email templating, and then we'll configure local test sending via SMTP.

Dmitry Berdnikov

Frontend Developer

Why go through all this?

On one of our projects, we actively use HTML emails. Initially, templates were placed directly in the backend microservices - scattered and with repetitive parts. This created difficulties in maintenance and email testing. We wanted to simplify the development process, and most importantly - make email templating and checking convenient.

And as we know, email templating is painful. It's like you're developing for Internet Explorer - if you even remember what that is.

Moving Emails to a Separate Repository

We decided to create a separate repository where all email templates would be stored. The backend would be able to pull them centrally.

The next step was choosing tools for templating and testing. We considered:

  • MJML
  • Maizzle
  • Foundation HTML

We wanted to write in a higher-level language, using pre-built components. With Maizzle and Foundation HTML, it seemed like we'd have to write more raw HTML code.

In the end, we settled on MJML - a markup language for email templates that compiles into full-fledged HTML, adapted for the specifics of email clients.

What is MJML and Why is it Convenient?

MJML is a language designed for HTML email templating, which compiles into regular HTML. This means you can write in an abstract syntax that then turns into HTML code with support for different mail clients. This eases adaptation for various mail clients and gives the ability to use:

  • responsive templating;
  • pre-built components;
  • code reuse with mj-include.

You can see how MJML code turns into HTML here.

Basic Project Setup

Let's go through the basic project setup - without delving into MJML syntax.

Install dependencies:

npm install mjml live-server concurrently

What each one does:

  • mjml - compilation MJML → HTML
  • live-server - starting a dev server with live reload (you can use any server)
  • concurrently - running commands in parallel

Configuring package.json

Add to scripts:

"scripts": {

  "start": "mjml --watch ./src/templates/**/*.mjml --output ./templates",

  "server": "live-server --host=localhost --watch=templates --open=templates --ignorePattern=\".*.mjml\"",

  "dev": "concurrently \"npm run start\" \"npm run server\"",

  "build": "mjml ./src/templates/**/*.mjml --output ./templates"

}

What each command does:

  • start - watches *.mjml files in src/templates/, compiles them to ./templates
  • server - starts a server and tracks changes in ./templates
  • dev - runs start and server in parallel
  • build - one-time template compilation without a watcher

Creating the First Template

Create a file example.mjml at /src/templates/example.mjml. All templates will be in the /src/templates folder. Add the following code to the file:

<mjml>
  <mj-body background-color="#f5f5f5">
    <mj-section padding="40px 0 20px">
      <mj-column>
        <mj-text align="center" font-size="28px" font-weight="bold">Email built with MJML</mj-text>
      </mj-column>
    </mj-section>
    <mj-section background-color="#ffffff">
      <mj-column>
        <mj-image src="https://placehold.jp/300x200.png" />
      </mj-column>
      <mj-column>
        <mj-text font-size="18px" font-weight="bold">Hello, world!</mj-text>
        <mj-text>This email was sent using MJML.</mj-text>
      </mj-column>
    </mj-section>
    <mj-section>
      <mj-column>
        <mj-button href="#" background-color="#4CAF50">Subscribe</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Run npm run dev - and a browser will open with the HTML generated from MJML. Each time the template changes, the page will automatically update. It's also useful to create a ./src/parts folder for reusable layout components.

As a result, we'll get desktop and mobile versions.

Desktop version
Mobile version

In the ./src/parts folder, create reusable email parts:

./src/parts/global-settings.mjml

<mj-style inline="inline">
  body { background-color: #f0f4f6; font-family: Arial, sans-serif; }
  a { color: #1d5cdb; text-decoration: none; }
</mj-style>
<mj-attributes>
  <mj-text
    font-size="17px"
    line-height="24px"
    color="#000"
    padding-top="5px"
    padding-bottom="5px"
  />
</mj-attributes>

./src/parts/header.mjml

<mj-section>
  <mj-column>
    <mj-image
      src="https://placehold.jp/82x47.png"
      alt="Logo"
      width="82px"
      height="47px"
    />
  </mj-column>
</mj-section>

Let's include them in example.mjml and remove unnecessary styles:

<mjml>
  <mj-head>
    <mj-include path="../parts/global-settings.mjml" />
    <mj-title>Example</mj-title>
  </mj-head>
  <mj-body>
    <mj-include path="../parts/header.mjml" />
    <mj-section padding="40px 0 20px">
      <mj-column>
        <mj-text align="center" font-size="28px" font-weight="bold">Email built with MJML</mj-text>
      </mj-column>
    </mj-section>
    <mj-section background-color="#ffffff">
      <mj-column>
        <mj-image src="https://placehold.jp/300x200.png" />
      </mj-column>
      <mj-column>
        <mj-text font-size="18px" font-weight="bold">Hello, world!</mj-text>
        <mj-text>This email was sent using MJML.</mj-text>
      </mj-column>
    </mj-section>
    <mj-section>
      <mj-column>
        <mj-button href="#" background-color="#4CAF50">Subscribe</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

We get the result:

In summary:

  • We can globally set common styles and settings for components using global-settings.
  • We can similarly extract code parts, as done with header.
  • We develop and see changes in the browser in real-time thanks to the configured dev server.

Using Templates on the Backend (Go)

Our project backend is written in Go. The compiled email templates end up in the ./templates folder. From there, the backend can pull them and send emails. In our case, connecting templates looks like this:

1. In the project root - go.mod:

module gitlab.site.ru/front-html-email-templates

go 1.22.0

2. In ./templates/templates.go

package templates

import _ "embed"

//go:embed example.html

var Example string

When adding a new template, a similar variable needs to be added to templates.go.

Also, emails have variables that the backend substitutes using its templating engine. In our case, the syntax for variables is {{.variableName}}.

<mjml>
  <mj-body>
    <mj-section padding="40px 0 20px">
      <mj-column>
        <mj-text align="center" font-size="28px" font-weight="bold">{{.title}}</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

To understand what variables are needed in a template, we created a ./docs/templates folder where we store markdown files with the same name as the template and a description of which variables are used, for example:

## Candidate

Email template 'Application without a vacancy'

### Variables
- `{{.date}}` Application Date
- `{{.title}}` Vacancy Title
- `{{.region}}` Region
- `{{.name}}` Full Name
- `{{.phone}}` Phone
- `{{.email}}` Email
- `{{.comment}}` Comment
- `{{.resume_link}}` Resume Link
- `{{.year}}` Current Year

The variables themselves can be added to the files by the backend developer, and the frontend developer just needs to place them in the layout in the correct spots.

Local SMTP Testing

Let's write a simple JS script that will send our templates to a real email.

First, install the following packages:

npm install dotenv nodemailer

  • dotenv — package for loading .env files in JS;
  • nodemailer — package for sending emails with SMTP support.

After that, create a file ./send-test-email.js and add the code:

import nodemailer from 'nodemailer'
import dotenv from 'dotenv'
import fs from 'fs/promises'

dotenv.config()

async function sendTestEmail() {
  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: process.env.SMTP_PORT,
    secure: true, // true for port 587, false for other ports
    auth: {
      user: process.env.SEND_FROM_EMAIL,
      pass: process.env.SEND_FROM_EMAIL_PASSWORD,
    },
  })gmail.com

  const htmlEmailString = await fs.readFile(
    `./templates/${process.env.TEMPLATE_NAME}.html`,
    'utf-8'
  )

  const mailOptions = {
    from: `Test Sender <${process.env.SEND_FROM_EMAIL}>`,
    to: process.env.SEND_TO_EMAIL,
    subject: 'Test HTML Email',
    html: htmlEmailString,
  }

  const info = await transporter.sendMail(mailOptions)
  console.log('Message sent: %s', info.messageId)
}

sendTestEmail().catch(console.error)

This is a simple implementation for sending emails via SMTP. We also need to create a .env file with the necessary variables:

TEMPLATE_NAME=email-confirmation
SEND_TO_EMAIL=test@byteminds.co.uk
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SEND_FROM_EMAIL=test@gmail.com
SEND_FROM_EMAIL_PASSWORD=BcsftTdfdsf

  • TEMPLATE_NAME - the name of the template to be sent;
  • SEND_TO_EMAIL - where the email will be sent;
  • SMTP_HOST - SMTP host;
  • SMTP_PORT - port;
  • SEND_FROM_EMAIL - the sender's email address.
  • SEND_FROM_EMAIL_PASSWORD - the sender's email password.

Primarily, the TEMPLATE_NAME and SEND_TO_EMAIL variables change. For testing different templates and different mail clients.

The other variables only need to be configured once.

After all the setup, to send an email, you need to run the script:

node ./send-test-email.js

For convenience, you can add a script to package.json. You can also implement support for bulk sending a template to different mail clients.

Conclusion

The current concept can be applied with other tools as well. As a result, we have:

  • a centralized repository for HTML emails;
  • email templating with best practices and reusable parts;
  • local SMTP testing.

It's important to keep in mind that templating can still break in old Outlook clients and with solutions like MJML where standard templates are provided. Even using best practices doesn't completely solve this issue. To avoid "breaking" in old solutions, you can use very trivial layout: simple texts and headings, no styling or visual elements. 

It's easy to start working with us. Just fill the brief or call us.

Find out more
White Arrow
From the blog
Related articles

gRPC for Testers: Quick Start After REST

Vasil Khamidullin

This article will help you understand gRPC basics, how it differs from REST, the structure of .proto files, and most importantly- how to test gRPC services using Postman.

QA

Understanding CORS: A Practical Guide

Bair Ochirov

In this article, I will briefly answer questions about why the CORS policy was created, how it works, why a simple action like "setting a header on the backend" might not be enough, and what secure patterns to choose for the frontend.

cors
sop

How I Started Writing Unit Tests for Vue Components - Part 2

Dmitry Simonov

So, it's been a year since the last article, and a lot has changed. In this one, we're going to talk about integrating with Mock Service Worker (MSW).

vuejs
Vue

Performance Issues in Web Services: A Practical Guide to Identification and Resolution

Dmitry Bastron

Performance is not a feature to add later, but a core requirement as vital as security. This guide provides a structured approach to building performance into your web services from the start, ensuring they are fast and scalable by design.

Development
WebDev

Setting SMART Goals for Digital Product Success

Andrey Stepanov

This quick guide helps you define clear objectives and track progress effectively—ensuring every milestone counts.

Development

Building Teams for Digital Products: Essential Roles, Methods, and Real-World Advice

Andrey Stepanov

A digital product isn’t just about features or design—it’s about teamwork. In this article, we break down the essential roles in digital product development.

Development

Goals in Digital Development: How to Launch a Digital Product Without Failure

Andrey Stepanov

Essential tips for creating a development team that works toward product goals, not just tasks.

Development

How to Become a Kentico MVP

Dmitry Bastron

Hi, my name is Dmitry Bastron, and I am the Head of Development at ByteMinds. Today, I’d like to share how I achieved Kentico MVP status, why I chose this CMS, and what it takes to follow the same path and earn the coveted MVP badge.

Kentico

How can a team lead figure out a new project when nothing is clear?

Maria Serebrovskaya

Hello! My name is Maria, and I am a team lead and backend developer at ByteMinds. In this article, I will share my experience: I’ll explain how to understand a complex project, establish processes, and make it feel like "your own".

Development

How I Started Writing Unit Tests for Vue Components

Dmitry Simonov

In this article, we’ll take a look at how you can start testing Vue components.

Vue
vuejs

Inspecting raw database data in Xperience by Kentico

Dmitry Bastron

This article is a cheat sheet to inspect what's going on with the imported data by Xperience by Kentico Migration Toolkit, resolve some CI/CD issues, and on many other occasions!

Kentico

Learnings from using Sitecore ADM

Anna Bastron

Let's try to understand how the ADM module works, its limitations, and tactics for optimising its performance.

Sitecore

Your last migration to Xperience by Kentico

Dmitry Bastron

The more mature Xperience by Kentico products become, the more often I hear "How can we migrate there?”

Kentico

5 Key Software Architecture Principles for Starting Your Next Project

Andrey Stepanov

In this article, we will touch on where to start designing the architecture and how to make sure that you don’t have to redo it during the process.

Architecture
Software development

Assessing Algorithm Complexity in C#: Memory and Time Examples

Anton Vorotyncev

Today, we will talk about assessing algorithm complexity and clearly demonstrate how this complexity affects the performance of the code.

.NET

Top 8 B2B Client Service Trends to Watch in 2024

Tatiana Golovacheva

The development market today feels like a race - each lap is quicker, and one wrong move can cost you. In this race, excellent client service can either add extra points or lead to a loss due to high competition.

Customer Service
Client Service

8 Non-Obvious Vulnerabilities in E-Commerce Projects Built with NextJS

Dmitry Bastron

Ensuring security during development is crucial, especially as online and e-commerce services become more complex. To mitigate risks, we train developers in web security basics and regularly perform third-party penetration testing before launch.

Next.js
Development

How personalisation works in Sitecore XM Cloud

Anna Bastron

In my previous article, I shared a comprehensive troubleshooting guide for Sitecore XM Cloud tracking and personalisation. This article visualises what happens behind the scenes when you enable personalisation and tracking in your Sitecore XM Cloud applications.

Sitecore

Server and client components in Next.js: when, how, and why?

Sergei Pestov

All the text and examples in this article refer to Next.js 13.4 and newer versions, in which React Server Components have gained stable status and become the recommended approach for developing applications using Next.js.

Next.js

How to properly measure code speed in .NET

Anton Vorotyncev

Imagine you have a solution to a problem or a task, and now you need to evaluate the optimality of this solution from a performance perspective.

.NET

Formalizing API Workflow in .NET Microservices

Artyom Chernenko

Let's talk about how to organize the interaction of microservices in a large, long-lived product, both synchronously and asynchronously.

.NET

Hidden Aspects of TypeScript and How to Resolve Them

Dmitry Berdnikov

We suggest using a special editor to immediately check each example while reading the article. This editor is convenient because you can switch the TypeScript version in it.

TypeScript

Troubleshooting tracking and personalisation in Sitecore XM Cloud

Anna Gevel

One of the first things I tested in Sitecore XM Cloud was embedded tracking and personalisation capabilities. It has been really interesting to see what is available out-of-the-box, how much flexibility XM Cloud offers to marketing teams, and what is required from developers to set it up.

Sitecore

Mastering advanced tracking with Kentico Xperience

Dmitry Bastron

We will take you on a journey through a real-life scenario of implementing advanced tracking and analytics using Kentico Xperience 13 DXP.

Kentico
Devtools

Why is Kentico of such significance to us?

Anastasia Medvedeva

Kentico stands as one of our principal development tools. We believe it would be fitting to address why we opt to work with Kentico and why we allocate substantial time to cultivating our experts in this DXP.

Kentico

Where to start learning Sitecore - An interview with Sitecore MVP Anna Gevel

Anna Gevel

As a software development company, we at Byteminds truly believe that learning and sharing knowledge is one of the best ways of growing technical expertise.

Sitecore

Sitecore replatforming and upgrades

Anastasia Medvedeva

Our expertise spans full-scale builds and support to upgrades and replatforming.

Sitecore

How we improved page load speed for a Next.js e-commerce website by 50%

Sergei Pestov

How to stop the decline of the performance indicators of your e-commerce website and perform optimise page load performance.

Next.js

Sitecore integration with Azure Active Directory B2C

Dmitry Bastron

We would like to share our experience of integrating Sitecore 9.3 with Azure AD B2C (Azure Active Directory Business to Consumer) user management system.

Sitecore
Azure

Dynamic URL routing with Kontent.ai

We'll consider the top-to-bottom approach for modeling content relationships, as it is more user-friendly for content editors working in the Kontent.ai admin interface.

Kontent Ai

Headless CMS. Identifying Ideal Use Cases and Speeding Up Time-to-Market

Andrey Stepanov

All you need to know about Headless CMS. We also share knowledge about the benefits of Headless CMS, its pros and cons.

Headless CMS

Enterprise projects: what does a developer need to know?

Fedor Kiselev

Let's talk about what enterprise development is, what nuances enterprise projects may have, and which skills you need to acquire to successfully work within the .NET stack.

Development

Fixed Price, Time & Materials, and Retainer: How to Choose the Right Agreement for Your Project with Us

Andrey Stepanov

We will explain how these agreements differ from one another and what projects they are suitable for.

Customer success

Sitecore Personalize: tips & tricks for decision models and programmable nodes

Anna Gevel

We've collected various findings around decision models and programmable nodes working with Sitecore Personalize.

Sitecore

Umbraco replatforming and upgrades

Anastasia Medvedeva

Our team boasts several developers experienced in working with Umbraco, specialising in development, upgrading, and replatforming from other CMSs to Umbraco.

Umbraco

Kentico replatforming and upgrades

Anastasia Medvedeva

Since 2015, we've been harnessing Kentico's capabilities well beyond its core CMS functions.

Kentico

Interesting features of devtools for QA

Egor Yaroslavcev

Chrome DevTools serves as a developer console, offering an array of in-browser tools for constructing and debugging websites and applications.

Devtools
QA

Activity logging with Xperience by Kentico

Dmitry Bastron

We'll dive into practical implementation in your Xperience by Kentico project. We'll guide you through setting up a custom activity type and show you how to log visitor activities effectively.

Kentico
This website uses cookies. View Privacy Policy.