
There’s a moment in every product’s life where something real happens. A policy is born, a claim comes through or a loss is registered. And in that moment, a system somewhere has to decide what to say, and how to say it.
Not generically. Correctly. With warmth where warmth is needed, with precision where the law demands it, and in the language the customer actually speaks. Sometimes, that’s four languages.
This is how you build that.
The Weight of a Small Message
An email feels like nothing. It arrives, it gets read or ignored, and then it disappears into the archive. And yet, behind that modest surface lies a surprisingly heavy set of requirements: it must render correctly on every screen imaginable, speak in a human voice, carry live data from the system, a name, a date, a number, and do all of this reliably, at exactly the right moment in a customer’s journey.
So the question you will keep asking is this: how do you make something that feels personal when it’s being generated by a machine?
As it turns out, the answer is a design philosophy more than a technical trick.
Composing the Message and Making It Scale

Start with your rendering layer
For email, that means MJML, a framework designed specifically for this hostile territory. Email clients are notoriously fragmented: the same HTML that looks elegant in one client becomes broken in another, across desktop applications, mobile apps, and webmail. MJML abstracts all of that away. You describe what you want, a headline, a column, a text block, and it produces the kind of battle-hardened markup that survives the journey into any inbox. The moment you adopt it, you stop fighting the medium and start thinking about what to say.
Add a Templating Engine
Next, add a templating engine. On top of MJML, Nunjucks provides the intelligence. Nunjucks is the right choice here, a JavaScript-native engine with a lineage that traces back to Jinja, Python’s beloved answer to the same problem. Nunjucks is where the intelligence lives. It reads variables, evaluates conditions, loops over collections of content, and assembles the final shape of each message. This is the layer that decides: this customer gets a personalised salutation, that one gets a fallback. This email has three paragraphs, that one has five. The template itself does not change, only the context flowing into it does. That distinction matters more than it sounds, and it is the thing you need to internalize before you write a single line.
Compose, Don’t Duplicate
Instead, compose your templates from reusable building blocks. Each email declares which building blocks it needs, a header, a greeting, a body, a closing and the system assembles them at generation time, pulling each block from a shared library. This is the rule: if a piece of content or structure appears in more than one place, it lives in exactly one place. Updating it updates everything that references it. Composition over repetition, carried to its logical conclusion.
Separate Language from Structure
For multilingual support, you need a parallel layer of translation file, one JSON per language, that holds every piece of human-readable copy the system will ever produce. The generation process reads the appropriate file, resolves every key to its value, and injects the result into the assembled template. The same structural skeleton speaks English in one pass, Italian in the next, without any part of the layout needing to know which language it is rendering. Keep these files entirely separate from your templates. Copy belongs to writers. Structure belongs to engineers. They should meet by design, not by necessity.
Automate the Build
Finally, you need to orchestrate all of this with a build pipeline. Gulp works well for this. The task runner coordinates the sequence: load the templates, load the translations, combine them through the templating engine, compile the result through MJML, and write the output. You run it once, for every language, for every template, in a single invocation. An email shouldn’t exist as just an HTML file. It should be accompanied by a structured descriptor containing the metadata your dispatch platform needs to route, schedule, and deliver the message. Every email, in every language, becomes two artifacts: the message itself and the descriptor that tells the system what to do with it. The pipeline produces both. The dispatch service simply executes the instructions.
What is remarkable about this stack is not any single technology in it. It is the discipline of keeping three layers, layout, logic, and language, cleanly separated from the start. As a result, the system remains simple even as it supports a surprisingly large surface area.
The Principle You Cannot Compromise On

Ultimately, the insight that makes all of this work is deceptively simple: keep structure and content completely separate, and never let either one know too much about the other.
Structure: the skeleton of an email, where the headline lives, where the greeting appears, how the content flows, should be defined once, in shared building blocks that every email reuses. A change to a building block should propagate everywhere it is used, automatically and consistently.
Content: every word a customer will read, should live separately, in translation files. A copywriter should be able to refine the tone of a rejection email without understanding how the underlying system works. A legal reviewer should be able to approve termination language in Italian without touching the structure. Otherwise, content and implementation become coupled, and every change becomes more expensive than the last.
Logic: which greeting to use, which body belongs to which stage of the journey, which conditions determine the final message, is resolved during generation. By the time the email reaches the customer, every decision has already been made. What they receive is simply a letter, one that feels as though it was written specifically for them.
Map the Relationship Before You Write the Templates
Before anything else, map the full arc of your customer’s journey. Every significant moment that deserves a message, from beginning to end.
Every journey begins somewhere: with curiosity, uncertainty, and eventually a decision. There is a middle that should feel steady rather than transactional. And there are endings, which deserve more care than anything else. If your domain includes real loss, that message deserves the greatest care of all. It should begin with acknowledgement, handle every administrative detail on the recipient’s behalf, and close with something approaching tenderness.
A system may generate the message. It should never read like one.
What You End Up With
One command, every template, every language. One consistent output.
That is what a well-designed system delivers. Change a contact number, a legal clause, or a line of copy, rebuild, and the entire output matrix updates automatically. No hunting through files. No forgotten translations. Every language stays in sync, so the English version never says something the Italian version does not. Version drift disappears and manual synchronisation becomes unnecessary.
What You Are Actually Building

Here is the thing nobody tells you at the start: you are not building just email templates.
You are building a system of communication that must hold up under pressure, when a customer is angry, when a payment has failed, when someone is grieving. It has to be right in that moment, in the right language, with the right tone. It has to feel like it came from a person who was paying attention.
The architecture is how you guarantee that. Structure, logic, language, kept apart, each doing only its job. A build process that is ruthlessly repeatable. A composition model that means every change propagates cleanly. Translation files that let the right people use the right words without ever touching the machinery.
Do all of that and you end up with something quietly remarkable: a system that scales without hardening, that maintains without breaking, and that, at the moments that matter most, manages to sound like a human being.
Build the structure. Trust the humans. Get out of the way.





