Меня зовут Дима. Я 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. Оттуда бэкенд может их подтягивать и отправлять письма. В нашем случае подключение шаблонов выглядит так:

  1. В корне проекта — go.mod:

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

go 1.22.0
  1. В ./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, где даются эталонные шаблоны писем. Даже использование лучших практик не избавляет от этой проблемы. Чтобы избежать «разваливания», для старых решений можно использовать максимально тривиальную верстку: простые тексты и заголовки, отсутствие стилизаций и визуальных элементов. Если вы знаете, как иначе можно выйти из ситуации, расскажите в комментариях – будет полезно ?

Комментарии (0)