Меня зовут Дима. Я Frontend разработчик в компании fuse8. В одном из проектов мы столкнулись с необходимостью упростить процесс верстки и тестирования HTML-писем. В итоге решили вынести шаблоны в отдельный репозиторий и собирать их с помощью MJML.
В этой статье разберём два ключевых этапа: сначала создадим репозиторий для верстки писем, а затем настроим локальную тестовую отправку через SMTP.
Вот ссылка на репозиторий, где можно взять готовый шаблон, запустить его и проверить. А о том, из чего он состоит и как формируется, читайте ниже.
Зачем всё это?
На проекте мы активно используем HTML-письма. Изначально шаблоны располагались прямо в микросервисах бэкенда — разрозненно, с повторяющимися частями. Это создавало сложности при сопровождении и тестировании писем. Захотелось упростить процесс разработки, а главное — сделать верстку и проверку писем удобной.
А верстать письма, как известно, больно. Это как будто вы верстаете под Internet Explorer — если вы, конечно, помните, что это такое.
Первый шаг: выносим письма в отдельный репозиторий
Мы решили создать отдельный репозиторий, в котором будут храниться все шаблоны писем. Бэкенд сможет подтягивать их централизованно.
Следующий шаг — выбор инструментов для верстки и тестирования. Рассматривались:
-
MJML
-
Maizzle
-
Foundation HTML
Хотелось писать на более высокоуровневом языке, используя предустановленные компоненты. В Maizzle и Foundation HTML показалось, что придется писать больше кода на чистом HTML.
В итоге остановились на MJML — языке разметки для email-писем, который компилируется в полноценный HTML, адаптированный под специфику email-клиентов.
Что такое MJML и чем он удобен?
MJML – это язык, предназначенный для верстки HTML писем, который компилируется в обычный HTML. Это значит, что можно писать в абстрактном синтаксисе, который затем превратится в HTML-код с поддержкой разных почтовых клиентов. Это облегчает адаптацию под разные почтовые клиенты и даёт возможность использовать:
-
адаптивную верстку;
-
предустановленные компоненты;
-
переиспользование кода с помощью mj-include.
Здесь можно посмотреть, как код mjml превращается в html.
Базовая настройка проекта
Разберём базовую настройку проекта — без углубления в синтаксис MJML.
Установим зависимости:
npm install mjml live-server concurrently
Что делает каждая из них:
-
mjml— компиляция MJML → HTML -
live-server— запуск dev-сервера с live reload (можно использовать любой сервер) -
concurrently— параллельный запуск команд
Настройка package.json
Добавим в 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" }
Что делает каждая команда:
-
start— следит за файлами*.mjmlвsrc/templates, компилирует в./templates -
server— поднимает сервер и отслеживает изменения в./templates -
dev— запускаетstartиserverпараллельно -
build— однократная сборка шаблонов без вотчера
Создаём первый шаблон
Создадим файл example.mjml по пути /src/templates/example.mjml, в папке /src/templates будут все шаблоны. В файл добавим следующий код:
<mjml> <mj-body background-color="#f5f5f5"> <mj-section padding="40px 0 20px"> <mj-column> <mj-text align="center" font-size="28px" font-weight="bold">Письмо на 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">Привет, мир!</mj-text> <mj-text>Это письмо отправлено с помощью MJML.</mj-text> </mj-column> </mj-section> <mj-section> <mj-column> <mj-button href="#" background-color="#4CAF50">Подписаться</mj-button> </mj-column> </mj-section> </mj-body> </mjml>
Запускаем npm run dev — и в браузере откроется HTML, сгенерированный из MJML. При каждом изменении шаблона страница будет автоматически обновляться. Также полезно создать папку ./src/parts для переиспользуемых частей верстки.
В результате получим десктопную и мобильную версии.
В папке ./src/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="Логотип" width="82px" height="47px" /> </mj-column> </mj-section>
Подключим их в example.mjml и удалим лишние стили:
<mjml> <mj-head> <mj-include path="../parts/global-settings.mjml" /> <mj-title>Пример</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">Письмо на 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">Привет, мир!</mj-text> <mj-text>Это письмо отправлено с помощью MJML.</mj-text> </mj-column> </mj-section> <mj-section> <mj-column> <mj-button href="#" background-color="#4CAF50">Подписаться</mj-button> </mj-column> </mj-section> </mj-body> </mjml>
Получаем результат:

В итоге:
-
Можем глобально задавать общие стили и настройки для компонентов с помощью global-settings.
-
Можем выносить по аналогии части кода, как это сделано в header.
-
Разрабатываем и видим изменения в браузере в реальном времени за счет настроенного dev сервера.
Использование шаблонов на бэкенде (Go)
У нас на проекте бэкенд написан на Go. Собранные шаблоны писем попадают в папку ./templates. Оттуда бэкенд может их подтягивать и отправлять письма. В нашем случае подключение шаблонов выглядит так:
-
В корне проекта —
go.mod:
module gitlab.site.ru/front-html-email-templates go 1.22.0
-
В
./templates/templates.go:
package templates import _ "embed" //go:embed example.html var Example string
При добавлении нового шаблона нужно добавить аналогичную переменную в templates.go.
Также в письмах есть переменные, которые подставляет бэкенд с помощью своего шаблонизатора. В нашем случае для переменных следующий синтаксис {{.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>
Чтобы понимать, какие переменные нужны в шаблоне, мы создали папку ./docs/templates, где хранятся md файлы с одноименным названием шаблона и описанием того, какие переменные используются, например:
## Candidate Шаблон письма 'Отклик без вакансии' ### Переменные - ``{{.date}}`` Дата отклика - ``{{.title}}`` Название вакансии - ``{{.region}}`` Регион - ``{{.name}}`` ФИО - ``{{.phone}}`` Телефон - ``{{.email}}`` Email - ``{{.comment}}`` Комментарий - ``{{.resume_link}}`` Ссылка на резюме - ``{{.year}}`` Актуальный год отправки письма
Сами переменные в файлы может занести бэкендер, а фронтендеру остается расставить их в верстке по правильным местам.
Локальное тестирование по smtp
Напишем простой js скрипт, который будет отправлять наши шаблоны на реальную почту.
Для начала установим следующие пакеты:
npm install dotenv nodemailer
-
dotenv— пакет для загрузки .env файлов в js; -
nodemailer— пакет для отправки писем с поддержкой SMTP.
После этого создадим файл ./send-test-email.js и добавим код:
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 для 465, false для других портов auth: { user: process.env.SEND_FROM_EMAIL, pass: process.env.SEND_FROM_EMAIL_PASSWORD, }, }) 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)
Это простая реализация отправки письма по SMTP. Также нам нужно создать .env файл с нужными переменными:
TEMPLATE_NAME=email-confirmation SEND_TO_EMAIL=test@fuse8.online SMTP_HOST=smtp.mail.ru SMTP_PORT=465 SEND_FROM_EMAIL=test@mail.ru SEND_FROM_EMAIL_PASSWORD=BcsftTdfdsf
-
TEMPLATE_NAME— название шаблона, который будет отправлен; -
SEND_TO_EMAIL— куда будет отправлено письмо; -
SMTP_HOST— smtp хост; -
SMTP_PORT— порт; -
SEND_FROM_EMAIL— откуда будет уходить письмо. -
SEND_FROM_EMAIL_PASSWORD— пароль от отправляемой почты.
В основном меняются переменные TEMPLATE_NAME и SEND_TO_EMAIL. Для тестирования разных шаблонов и разных почтовых клиентов.
Остальные переменные нужно настроить один раз. Например, как подключиться по SMTP в mail почте https://help.mail.ru/mail/login/mailer.
После всей настройки для отправки письма нужно вызвать скрипт:
node ./send-test-email.js
Для удобства можно добавить скрипт в package.json, также можно сделать поддержку массовой отправки шаблона на разные почтовые клиенты.
Заключение
Текущую концепцию можно применить и с другими инструментами. В результате у нас есть:
-
централизованный репозиторий для html писем;
-
верстка писем с лучшими практиками и переиспользуемыми частями;
-
локальное тестирование по smtp.
Нужно иметь в виду, что верстка все равно может разваливаться в старых outlook клиентах и с такими решениями как MJML, где даются эталонные шаблоны писем. Даже использование лучших практик не избавляет от этой проблемы. Чтобы избежать «разваливания», для старых решений можно использовать максимально тривиальную верстку: простые тексты и заголовки, отсутствие стилизаций и визуальных элементов. Если вы знаете, как иначе можно выйти из ситуации, расскажите в комментариях – будет полезно 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/937538/
Добавить комментарий