Building Responsive Email Systems with MJML at Scale
The Email Rendering Problem
Building emails that look good across every client is one of the most frustrating challenges in web development. Outlook uses Word's rendering engine. Gmail strips inline styles selectively. Apple Mail supports modern CSS while most webmail clients do not. I spent years wrestling with this problem at Dyson before discovering MJML, and it transformed my approach entirely.
MJML is a markup language that compiles to responsive HTML email. You write clean, readable markup and get battle-tested HTML that works everywhere.
Why MJML Over Raw HTML
Before MJML, I was writing email HTML by hand. Nested tables, inline styles, conditional comments for Outlook: it was painful and error-prone. A single email template could be 500 lines of incomprehensible HTML. With MJML, the same template is 50 lines of clean markup:
<mjml>
<mj-body>
<mj-section background-color="#ffffff">
<mj-column>
<mj-image src="https://example.com/logo.png" width="150px" />
<mj-text font-size="20px" font-family="Arial">
Welcome to Our Platform
</mj-text>
<mj-text>
{{BODY_CONTENT}}
</mj-text>
<mj-button background-color="#4d94d4" href="{{CTA_URL}}">
Get Started
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
This compiles to several hundred lines of responsive HTML that works in every major email client.
Building a Template System
For production email at scale, you need a template system, not individual email files. I build my templates as composable MJML components:
- Base layout: Header, footer, and overall structure shared across all emails
- Content blocks: Reusable sections for different content types (text, images, CTAs, product cards)
- Theme variables: Colors, fonts, and spacing defined once and referenced throughout
The Compilation Pipeline
import subprocess
import json
def compile_mjml(template: str, variables: dict) -> str:
# Inject variables into MJML template
for key, value in variables.items():
template = template.replace(f"{{{{{key}}}}}", value)
# Compile MJML to HTML
result = subprocess.run(
['mjml', '--stdin'],
input=template,
capture_output=True,
text=True
)
if result.returncode != 0:
raise ValueError(f"MJML compilation failed: {result.stderr}")
return result.stdout
AI-Powered Email Content Generation
Where things get interesting is combining MJML templates with AI-generated content. My email pipeline works like this:
- Define the email type and audience
- Generate personalized content with Claude or GPT-4o
- Inject the content into the appropriate MJML template
- Compile to HTML
- Run through a rendering test
- Send via the email service provider
The AI generates the text content. MJML handles the presentation. This separation of concerns means I can update templates without touching content logic and vice versa.
Testing at Scale
Email testing is notoriously time-consuming. I automated it with a combination of tools:
- MJML validation: Catches markup errors before compilation
- HTML size check: Gmail clips emails over 102KB. I flag any email approaching this limit
- Link validation: Automated checking of every URL in the compiled HTML
- Rendering preview: I use Litmus or Email on Acid for cross-client rendering tests on critical templates
Handling Personalization
Personalization at scale requires careful template design. Each variable needs a sensible fallback, and the layout must look good regardless of content length:
<mj-text>
Hi {{FIRST_NAME|there}},
</mj-text>
I also test each template with extreme content variations: very short names, very long names, empty optional fields, and content in different languages. MJML's responsive columns handle most layout variations gracefully, but you need to verify.
Production Volume
At Dyson, I managed email systems sending millions of messages per campaign. The lessons from that scale inform everything I build now:
- Always use a reputable email service provider. Never send directly from your server
- Implement proper SPF, DKIM, and DMARC records
- Monitor deliverability rates and bounce rates continuously
- Segment your audience and personalize content. One-size-fits-all emails perform poorly
- Test every campaign with a small sample before full deployment
MJML solved the hardest part of email development: making HTML that works everywhere. With that problem solved, you can focus on what actually matters: the content and the strategy.
Getting Started with MJML
Install MJML with npm, create your first template by adapting one of the official examples, and compile it. You will immediately see the difference between writing raw email HTML and writing MJML. Once you experience that difference, you will never go back to hand-coding email tables.