Skip to main content

Converting HTML Emails to MJML Components: Implementation Workflow

Why Migrate from Table-Based HTML to MJML

Legacy email markup depends on deeply nested <table> structures, client-specific conditional comments (<!--[if mso]>), and aggressive inline !important overrides. MJML replaces this with a declarative, component-driven abstraction layer that compiles to optimized, cross-client compatible HTML. Transitioning to Modern Email Templating Engines eliminates brittle layout inheritance, standardizes responsive breakpoints, and reduces maintenance overhead across transactional and marketing pipelines.

Pre-Conversion DOM Sanitization

Raw HTML must be normalized before MJML mapping. Unsanitized markup triggers parser failures and bloats final payloads.

  1. Strip Unsupported CSS: Remove @media queries targeting legacy clients (Outlook 2003/2007, Lotus Notes). MJML handles responsive breakpoints natively via <mj-column> width attributes.
  2. Isolate Dynamic Logic: Extract template variables ({{ }}, {% %}, #if) into a separate configuration file. MJML's compiler will strip unrecognized tags unless explicitly wrapped.
  3. Flatten Table Depth: Reduce nesting to a maximum of 3 levels. Use a headless DOM parser (cheerio or jsdom) to audit structural complexity:
node -e "const cheerio = require('cheerio'); const html = require('fs').readFileSync('legacy.html','utf8'); const $ = cheerio.load(html); console.log('Max table depth:', Math.max(...Array.from($('table')).map(el => $(el).parents('table').length)));"
  1. Remove Spacer Assets: Delete 1x1 transparent GIFs and empty <td> cells used for spacing. MJML uses padding, margin, and mj-spacer for predictable layout control.

Step-by-Step Implementation Pipeline

Step 1: Map Layout to MJML Primitives

Translate table semantics directly to MJML hierarchy. Maintain strict XML compliance.

<!-- Legacy HTML -->
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:#f4f4f4;">
 <tr>
 <td align="center" style="padding: 24px 0;">
 <table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;">
 <tr><td style="padding: 20px; font-family: sans-serif;">Header Content</td></tr>
 </table>
 </td>
 </tr>
</table>

<!-- MJML Equivalent -->
<mjml>
 <mj-body background-color="#f4f4f4">
 <mj-section padding="24px 0">
 <mj-column width="600px" background-color="#ffffff">
 <mj-text padding="20px" font-family="sans-serif">Header Content</mj-text>
 </mj-column>
 </mj-section>
 </mj-body>
</mjml>

Apply mj-class for reusable styling to enforce DRY principles and align with MJML Component Architecture standards.

Step 2: Handle Conditional Logic & Assets

MJML does not process server-side templating natively. Wrap dynamic blocks in <mj-raw> to bypass the compiler's XML parser:

<mj-raw>
 {% if user.tier == 'enterprise' %}
</mj-raw>
<mj-section>
 <mj-column>
 <mj-text>Enterprise Feature Block</mj-text>
 </mj-column>
</mj-section>
<mj-raw>
 {% endif %}
</mj-raw>
  • Images: Use absolute URLs only. Specify width and height explicitly to prevent layout shifts in Gmail/Apple Mail.
  • Backgrounds: Replace inline background-image CSS with <mj-image> or <mj-wrapper background-url="...">. MJML automatically generates VML fallbacks for Outlook.

Step 3: Compile & Optimize

Run the MJML CLI with production flags. Disable beautification to reduce whitespace overhead:

npx mjml@latest input.mjml -o output.html --config.beautify false --config.minify true

Debugging Rendering Discrepancies

Error Handling & Validation

  • XML Syntax Errors: MJML compilation halts on unclosed tags or invalid attributes. Pre-validate with xmllint:
xmllint --noout input.mjml 2>&1 | grep -i "error" && exit 1
  • Parser Stripping: If dynamic tags disappear in output, verify they are strictly enclosed in <mj-raw>. MJML v5+ enforces stricter XML parsing than v4.
  • CLI Version Mismatch: Pin dependencies in package.json ("mjml": "^4.15.0"). Breaking changes between v4 and v5 affect mj-style inheritance and mj-raw placement.

Payload & Rendering Optimization

  1. Size Audit: Transactional providers (SendGrid, SES, Postmark) enforce strict payload limits. Keep compiled HTML ≤ 102KB. Strip unused CSS classes post-compilation using email-comb:
npx email-comb output.html -o optimized.html --keep-class="mj-raw"
  1. Outlook VML Conflicts: MJML auto-generates VML for backgrounds. If custom VML is required, disable MJML's auto-generation via <mj-wrapper background-color="..." background-repeat="no-repeat"> and inject raw VML manually inside <mj-raw>.
  2. Regression Testing Pipeline:
# 1. Compile
mjml template.mjml -o template.html --config.minify true
# 2. Validate HTML5/Email spec
npx html-validate template.html
# 3. Diff against baseline (CI)
git diff --exit-code baseline.html template.html || echo "Rendering drift detected"
# 4. Push to rendering QA (Litmus/Email on Acid API)
curl -X POST https://api.litmus.com/v1/tests \
-H "Authorization: Bearer $LITMUS_TOKEN" \
-F "html_file=@template.html"

Implement automated snapshot testing in CI/CD to catch client-specific breaks before deployment. Maintain strict version control for .mjml source files and compiled .html outputs to ensure transactional delivery stability.