ADR-001: Custom Email Renderer Over MJML
Why Owlat built a custom table-based HTML email renderer instead of using MJML, gaining full control over VML, dark mode, and per-client rendering.
- Status: Accepted
- Date: 2024-06-15
Context
Owlat needs to convert a JSON block structure into responsive HTML emails that render correctly across all major email clients (Gmail, Outlook, Apple Mail, Yahoo). MJML is the industry standard for this, providing a markup language that compiles to email-safe HTML.
However, MJML has limitations for our use case:
- No fine-grained control — MJML abstracts away the underlying table-based HTML, making it difficult to implement advanced features like VML backgrounds, conditional content, or per-client rendering.
- Bundle size — MJML is a large dependency (~2MB) that would bloat the serverless backend.
- Block model mismatch — Our editor uses a JSON block model (similar to Notion/Editor.js). Converting blocks to MJML markup only to have MJML convert it to HTML adds an unnecessary translation layer.
- Extensibility — Adding custom block types in MJML requires writing MJML components that conform to its internal API, which is poorly documented and tightly coupled to its rendering pipeline.
Decision
Build a custom table-based HTML email renderer (@owlat/email-renderer) that converts our JSON block format directly to email-safe HTML.
Consequences
Enables:
- Direct control over every HTML attribute, enabling VML support for Outlook (bulletproof buttons, background images, gradients)
- Per-client render simulation (
targetClientoption) for accurate previews - Custom block registry for third-party block types
- CSS inlining with selective annotations (
@inline/@head-only) - Dark mode support with per-block overrides
- AMP email output as a first-class format
- Email size analysis and compatibility scoring
- Smaller bundle — the renderer is ~50KB vs MJML's ~2MB
Trade-offs:
- We own the full complexity of email client quirks (Outlook conditional comments, Gmail CSS stripping, Yahoo spacing bugs)
- New team members can't rely on MJML documentation — they need to learn our renderer's API
- We must maintain our own compatibility data instead of relying on MJML's tested output