MJML – Responsive Mailing

23 August 2019 Anna

Responsive, scalable, interactive and bulletproof … emails? ‘ln our dreams.’ we thought, but this is how the client wanted it to be: dreamlike mobile firstmailing. Learn more about the story of how with efficient communication, a bit of luck and a great MJML tool we have entered a new level of e-mail coding in a few hours. You will find here pieces of code showing implementation in a Symfony-based project.

Are you not interested in this story? Jump to »Implementation Details«

Introduction

For many systems, mailing is the main communication channel with the client. Templates must be visually appealing, responsive and eye-catching, and, above all, they should unambiguously provide information on all devices that the e-mail can be read on. Marketing specialists and UX designers create sophisticated graphic designs, but at the end there is usually a developer who must – colloquially speaking – ‘code up’ the template.

The Problem

As the system began to be created, there were not much emails, and we were focused on other, crucial issues. As it developed, the number of types of messages that could be sent has increased to several dozen templates. The nature of messages sent has also changed. From now on, emails were meant to attract new customers and thus simply to be a mean of advertising.

Before the time came to expand the software, we decided to auditour mailing in Email on Acid and … a cat came out of the bag. We made a safe, scalable backend. And what on the “front”? It turned out that despite the minimalistic, very conservative design, our e-mails collapse everywhere: in Outlook, Times New Roman is displayed instead of the intended font, images remain undecoded in Gmail, and the buttons are absolutely unsuitable for clicking … The task with the highest priority level fell on the board to clean up the mess – to fix dozens of templates full of tables and buttons.

Solution

Originally, we intended to correct e-mails on a linear basis, for each difficult e-mail client separately, but it quickly turned out to be too time-consuming. A few hours later, it has become obvious that we need a different solution.

We considered a few options, including using a simple style inliner or to simplify the templates even more, but all these ideas were insufficient. Then, we decided to choose something else: MJML. (https://mjml.io/)

MJML is a tool that provides the markup language and its ‘compiler’ written in Node.js. It rewrites the code to HTML so that the e-mail will be displayed correctly on as many popular e-mail clients as possible. The volume of HTML generated is about ten times larger than the volume of the MJML code, because everything becomes nested tables and tags dedicated to Outlooks are added. MJML documentation is great and a lot of ready-made templates that illustrate the effects that we can get are available. Not every layout can be done in this (if it can not, then suspect that this is not a good idea), but the initial configuration in the Symfony versions 2.8 up is child’s play – we’ll find a ready bundle there.

With MJML, you get as much responsiveness as you can get from the e-mail: a column-based layout, they can be grouped to make them be available in various configurations on the mobile. We will also find in it a lot of helper components for pictures, links, backgrounds, as well as the option to add a preheader. The developers included the option to write in plain HTML and to add your one’s own styles. They made available an option to inline styles or leave segments in the e-mail head. Unfortunately, even MJML does not solve all problems. By choosing this solution, among other things, we had to handle Blue Links and crop the images to the required size (to be displayed in old Outlooks under Windows 7 in the desired way). The mj-buttoncomponent turned out to be not very successful – but, considering that it was meant as a button in the e-mail – it works better than okay. It lacks rounded borders in Outlooks and it is only clickable on the text. When designing e-mails, it is still better to avoid tables or pictures in the backgrounds.

The Second Stage

MJML emails moved to production, the customer had no more reservations about them, so it’s time to expand. We received an order to rewrite the way 30 emails templates looked previously to make them look in accordance with the new graphic requirements. The templates also needed to appear in different brandsdepending on the context (logo, translations, colours were to be changed) again.

Once we analysed the graphics provided by the client, we separated about 20 types of components repeated in various e-mails that we extracted to separate .mjml.twig files. We have made separate spaces for the blocks with different versions of branding (including logotype, login button, contact numbers). The header and footer, that were the same in each e-mail, had been previously separated. All you had to do was to sort the code.

Ultimately, all you had to do with a single e-mail that can appear in different brands is simply to compose it of generic blocks, while the developer who implements the new template does not even see the MJML code (it is obscured by the Twig modules). The coding time of one email has been reduced from 8 to less than 2 hours, and all messages are visually consistent.

Implementation Details

We used a finished bundle: https://github.com/notFloran/mjml-bundle

Configuration options for Symfony 2.8

  1. Default
    in this case, in the production environment we will need Node.js configured so that the PHP application can launch it itself (this note applies especially to the managersof containerised environments) and mjml itself upliftedto node_modules;
  2. Own solution:
    implementation of the Renderer interface from the notFloran/mjml-bundle takes MJML, returns HTML, it is up to the developer to decide how this will happen;
  3. Integration with the official API MJML:

Sending all your emails to the external site over HTTP to handle rewriting the code into HTML.

We rejected the third solution on the spot: we were not sure if we were allowed to send mailing to an external site. We had to abandon the second one because of the time. The default configuration with its own executable file to be converted turned out to be sufficient. We have completed it in accordance with the documentation:
https://github.com/notFloran/mjml-bundle/blob/v1.0.0/README.md

Advantages:

  1. It took us 15 minutes to write a working email configuration on the local environment.
  2. In the production environment, it was necessary to uplift the node.js to the container with the application, which took another 15 minutes.
  3. We do not send emails to any external API: this way we do not become dependent on any external systems.

Comments:

  1. Each time an e-mail is rendered, Node.js must be started up and it takes about 400-700ms to process it. We considered writing our own solution with the idea that the templates will be converted from MJML to HTML with assets during the build phase and there will only be places for variables from Twig, but so far, we have not carried out this idea, because there was no need to do so.
  2. While setting up, Symfony 2.8 + mjml4, bundle will not minify e-mails because the way the mjml2html parameters are specified has changed between versions 3 and 4, while version 1 bundle formally only works with mjml3. However, this is the only version incompatibility issue that can be easily repaired: all you have to do is to replace the Renderer, which will do the same as the native one, but it will handle the set up options in a different way.
code

Parts of the code
For the purpose of making the entry, I decided to cut out irrelevant parts of the code, remove the PHPDoc blocks and add comments in Polish.

Service for sending e-mails:

//...
use NotFloranMjmlBundleMjml;
 
//...
 
class SendEmailMessageService
{
    private $mailer;
    private $engine;
    private $mjml;
 
    //    ...
    public function __construct(Swift_Mailer $mailer, Twig_Environment $engine, Mjml $mjml /* ... */)
    {
        $this->mailer = $mailer;
        $this->engine = $engine;
        $this->mjml = $mjml;
        //...
    }
 
    public function send(EmailMessageInterface $message): void
    {
        //...  Extracting subject, subject, recipient, sender, UDW and attachments from $message, dispatch events about sending an email
        $body = $this->getBody($message);
 
        //... building an email object, setting attachments and headers
        $this->mailer->send($mail);
    }
 
    private function getBody(EmailMessageInterface $message): string
    {
        return $this->mjml
            ->render($this->engine->load($message->getTemplate())
                ->renderBlock('body', $message->getParameters()));
    }
    //... 
}

Sample email template:

{# Here is the subject of the email, conceptually it is the view part of the email, so it can be placed in twig #}
{% block subject %}
    {{ 'Limit has been set'|trans({}, 'mail')|raw }}
{% endblock %}

{# The following part is rendered in SendEmailMessageService as the body of the mail #}
{% block body %}
    {% set layoutPath = '@BackendMailing/email/' ~ layout|default('')|lower ~ 'default' %} {# here we combine the path to catalog with layout of the given version #}
    {% set commonPath = '@BackendMailing/email/common' %}

    {% embed layoutPath ~ '/layout.html.twig' %} {# At this point, we embed the email in its layout in which there are a header and a footer #}
        {% block preheader %}
            {{ 'We are happy with you'|trans({}, 'mail') }}
        {% endblock %}
        {% block content %}
            {% include layoutPath ~ '/block/informal_greetings.html.twig' %} 
      {# The block path is taken into account with the layoutPath variable, because the salutation varies between versions #}
            {% include commonPath ~ '/block/spacer.html.twig' with {height: '15px'} %} 
            {# The block path is taken from the common space, because the vertical space looks the same regardless of the version #}
            {% embed commonPath ~ '/block/text.html.twig' with {fontSize: 'large'} %}
                {% block text %}
                    {{ 'Thank you for submitting your application to our service.'|trans({}, 'mail')  }}
                {% endblock %}
            {% endembed %}
            {% include commonPath ~ '/block/image.html.twig' with {imgSrc: '/mail/common/limit.png', width: '270px'} %}
            {% include commonPath ~ '/block/spacer.html.twig' with {height: '15px'} %}
            {% embed commonPath ~ '/block/text.html.twig' with { sideMargins: 10 } %} 
            {# Blocks can take parameters e.g. margin size, font size etc. #}
                {% block text %}
                    {{ 'We are in the process of verifying the invoice financing application we have granted you a limit of %limit%'|trans({'%limit%': limit|money_currency}, 'mail')|raw  }}
                {% endblock %}
            {% endembed %}
            {% include commonPath ~ '/block/spacer.html.twig' with {height: '20px'} %}
            {% include layoutPath ~ '/block/login_button.html.twig' %}
            {% include commonPath ~ '/block/spacer.html.twig' with {height: '30px', backgroundColor: '#f8f8f8'} %} 
            {% embed commonPath ~ '/block/footer_first_text.html.twig' %}
                {% block text %}
                    {{ 'You received this email because you showed your willingness to fund your company with us.'|trans({}, 'mail') }}<br/>
                    {% include layoutPath ~ '/block/footer-contact.html.twig' ignore missing %}
                {% endblock %}
            {% endembed %}
        {% endblock %}
    {% endembed %}
{% endblock %}

Sample layout:

<mjml>
    <mj-head>
        <mj-style>{# styles that let you get rid of Blue Links #}
            a[x-apple-data-detectors] {
            color: inherit !important;
            text-decoration: none !important;
            font-size: inherit !important;
            font-family: inherit !important;
            font-weight: inherit !important;
            line-height: inherit !important;
            }
            a {
            color: inherit !important;
            }
            .im {
            color: inherit !important;
            }
        </mj-style>
        <mj-style inline="inline">
            .cf {
            color: #b2b3b6 !important;
            }
            .cb {
            color: #58595A !important;
            }
        </mj-style>
        <mj-title>Title</mj-title>
        <mj-attributes>
            <mj-class name="no-border" border-style="hidden"/>
            <mj-all font-family="Verdana, Helvetica, sans-serif" color="#58595A"></mj-all>
            <mj-text align="center" font-weight="300" font-size="16px" line-height="24px"></mj-text>
            <mj-section padding="0px" background-color="#ffffff"></mj-section>
        </mj-attributes>
        <mj-preview>
            {{ block('preheader') }} {# we render a block that is in the templates of some emails #}
        </mj-preview>
    </mj-head>
    <mj-body background-color="#f8f8f8" width="700px">
        <mj-section background-color="#f8f8f8">
            <mj-column>
                <mj-spacer height="50px"/>
            </mj-column>
        </mj-section>
        {{ block('content') }} {# rendering a block which is in every template #}
        <mj-section align="center" background-color="#f8f8f8">
            <mj-column width="15%">
            </mj-column>
            <mj-column align="center" width="70%">
                <mj-table>
                    <tr style="list-style: none;line-height:1">
                        <td style="text-align: center">
                            <a href="https://www.facebook.com">
                                <img width="40" src="{{ CDN('/mail/common/socials/facebook-logo.png') }}"/>
                            </a>
                        </td>
                        <td style="text-align: center">
                            <a href="https://www.linkedin.com">
                                <img width="40" src="{{ CDN('/mail/common/socials/linkedin-logo.png') }}"/>
                            </a>
                        </td>
                        <td style="text-align: center">
                            <a href="https://www.slideshare.net">
                                <img width="40" src="{{ CDN('/mail/common/socials/slideshare-logo.png') }}"/>
                            </a>
                        </td>
                        <td style="text-align: center">
                            <a href="https://twitter.com">
                                <img width="40" src="{{ CDN('/mail/common/socials/twitter-logo.png') }}"/>
                            </a>
                        </td>
                        <td style="text-align: center">
                            <a href="https://www.youtube.com">
                                <img width="40" src="{{ CDN('/mail/common/socials/youtube-logo.png') }}"/>
                            </a>
                        </td>
                    </tr>
                </mj-table>
            </mj-column>
            <mj-column width="15%">
            </mj-column>
        </mj-section>
        <mj-section background-color="#f8f8f8">
            <mj-column>
                <mj-text color="#b2b3b6" line-height="1.3" font-size="11px">
                    ...
                </mj-text>
            </mj-column>
        </mj-section>
    </mj-body>
</mjml>

Sample module:

{# text.html.twig #}
{% set defSideMargins = "7" %}
{% set defFontSize = '15px' %}
{% set defAlign = 'center' %}
{% set defBackgroundColor = '#ffffff' %}
{% set defColor = '#5A656F' %}
{% set defLineHeight = '1.3' %}
{% set defPadding = "0px" %}

{% if sideMargins is defined and sideMargins is not null and '0' == sideMargins %}
    {% set textSidePadding = false %}
{% endif %}

{% if fontSize is defined and fontSize is not null and fontSize %}
    {% if 'small' == fontSize %}
        {% set fontSize = '10px' %}
    {% elseif 'medium' == fontSize %}
        {% set fontSize = '15px' %}
    {% elseif 'large' == fontSize %}
        {% set fontSize = '17px' %}
    {% endif %}
{% endif %}

{% block text_outer %}
    <mj-section background-color="{{ backgroundColor|default(defBackgroundColor) }}">
        <mj-column width="{{ sideMargins|default(defSideMargins) }}%"></mj-column>
        {# Instead of empty columns, you can just add padding on <mj-text>,
         ut then it will also appear in mobile version, and we don't want it there #}
        <mj-column width="{{ (100 - sideMargins|default(defSideMargins) * 2) }}%">
            <mj-text
                    font-size="{{ fontSize|default(defFontSize) }}"
                    align="{{ align|default(defAlign) }}"
                    color="{{ color|default(defColor) }}"
                    line-height="{{ lineHeight|default(defLineHeight) }}"
                    padding="{{ defPadding }}"
                    {% if isBold is defined and true == isBold %}
                        font-weight="bold"
                    {% else %}
                        font-weight="normal"
                    {% endif %}
                    {% if textSidePadding is defined and false == textSidePadding %}
                        padding-left="0px"
                        padding-right="0px"
                    {% endif %}
            >
                {{ block('text') }}
            </mj-text>
        </mj-column>
        <mj-column width="{{ sideMargins|default(defSideMargins) }}%"></mj-column>
    </mj-section>
{% endblock %}

The module, repeated in many e-mails:

{% embed '@BackendMailing/email/common/block/text.html.twig' with {size: 'small'} %}
    {# This block uses the text.html.twig block, but predefines variables values ​​and content #}
    {% block content %}
        {{ 'The message was generated automatically. Please do not reply.'|trans({},'mail') }}
    {% endblock %}
{% endembed %}

During the e-mail sending process, the application prepares the instance of the class that implements EmailMessageInterface and drops it in the sendmethod of the main the e-mail sending service. Of course, we have covered every type of e-mail sending with a dedicated site whose only task is to create a Message object and dropping it to the main e-mail service. Then, once in the main site, rendering is done: first, the version is selected, and based on it, blocks from the appropriate space are selected to properly style the e-mail. The main send servicereceives the MJML document rendered by the Twig engine, pushes it through the Renderer and, at this point, the email body is finished, and the subsequent process runs as the standard way.

code

And the time goes on

In retrospect, it has to be admitted that new layouts in pure HTML could not be coded in a short time (and still stay sane). Perhaps it could have been played better – move the entire mailing module to the microservice, become independent from Symfony 2.8 and Twig, but in the fire mode it should not have happened and it could not happen. If we consider the emergency response, then I think that this whole action was real smooth (and we will write the e-mail microservice anyway, but in the next stage).

The idea to use MJML was a grassroots movement and that’s what I like about Mint Software – no one sets boundaries to good ideas.

MARIA, Software Engineer

BPA implementations to our clients, read the history of their successes:

What can we do for you?

Do you have an idea, but your system is not fully satisfying?
Are you in need of dedicated software or process automation and robotisation?

Learn more about our services and arrange a meeting.