# Templates that compile once and can't be injected

> Email templates in Handlebars and MJML with versioning, typed variable schemas, and a compile-time lint that makes HTML injection structurally impossible.

Canonical: https://emailfast.dev/features/templates

## 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

1. 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.
2. Declare a typed schema per template: `string`, `email`, `number`, `boolean`, with `required`, `maxLen`, and `enum` constraints. A send whose `data` doesn't match is refused at admission.
3. Every edit creates a version instead of silently rewriting what came before.
4. 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

:::panel A typed variable schema
```json
{
  "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

:::tradeoffs Strictness you'll occasionally curse
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](/features/email-api) by id and power
[newsletters](/features/newsletters); the full authoring reference is in the
[docs](/docs). Deciding between platforms? The [comparison hub](/compare) is the
honest tour.

## 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 &quot;view entire message&quot; link — which usually hides your unsubscribe footer too. The editor warns at author time, while you can still shorten it.
