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.
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*.mjmlfiles insrc/templates/, compiles them to./templates -
server- starts a server and tracks changes in./templates -
dev- runsstartandserverin 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:
</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.
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:
- In the project root -
go.mod:
module gitlab.site.ru/front-html-email-templates
go 1.22.0
- 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.
Author: Dmitry Berdnikov



Top comments (0)