Templates that compile once and can't be injected
MJML for layout, Handlebars for data, typed variable schemas at the door, and a lint that makes unescaped output a build error instead of an incident.
Author once, render safely forever
Write MJML email templates with Handlebars variables and stop hand-assembling table HTML. Templates are versioned, every variable is typed, and the compile-time lint rejects any unescaped output — so template HTML-injection is structurally impossible rather than merely discouraged.
How it works
- Author in MJML for layout, Handlebars for data. MJML compiles at author time — never per-send — so sending renders merges of prebuilt HTML, and a broken layout surfaces when you save, not mid-campaign.
- Declare a typed schema per template:
string,email,number,boolean, withrequired,maxLen, andenumconstraints. A send whosedatadoesn't match is refused at admission. - Every edit creates a version instead of silently rewriting what came before.
- The lint runs at compile time and rejects every unescaped output. There is no "just this once" escape hatch — that absence is what makes the guarantee structural.
The editor also warns at author time when compiled HTML approaches Gmail's 102KB clipping threshold, while shortening it is still cheap.
The evidence
{
"name": { "type": "string", "required": true, "maxLen": 80 },
"email": { "type": "email", "required": true },
"seats": { "type": "number" },
"plan": { "type": "string", "enum": ["free", "starter", "growth"] }
}Four fields, four types, hard limits. A payload of { "plan": "<script>" } doesn't render harmlessly — it never gets in: rejected at admission, before a message exists.
Honest limits
There is no unescaped output, period — if a workflow needs raw HTML injected through a variable, the lint wins that argument by design. MJML's compiled output is table-heavy HTML because that's what renders consistently across mail clients; view-source is not where it shines. And the 102KB warning can only warn: when a message is too big, the fix is a shorter email, and nothing else.
Where to go next
Templates plug into the send API by id and power newsletters; the full authoring reference is in the docs. Deciding between platforms? The comparison hub is the honest tour.
Questions, answered plainly
How is template injection prevented?
Structurally. The compile-time lint rejects every unescaped output — a template containing one will not compile, so there is no path where user data becomes markup. It's a property of the system, not a review checklist.
When does MJML compile?
At author time, never per-send. Sending renders data merges of prebuilt HTML, so a layout mistake is caught when you save the template, not discovered halfway through a campaign.
What can I constrain in a variable schema?
Types (string, email, number, boolean) plus required, maxLen, and enum per variable. A send whose data doesn't match the schema is refused at admission, before a message exists.
What's the Gmail 102KB warning about?
Gmail clips messages whose HTML exceeds roughly 102KB, hiding the rest behind a "view entire message" link — which usually hides your unsubscribe footer too. The editor warns at author time, while you can still shorten it.