Prompt
How do I render HTML email templates with variables without breaking layouts?
Latest observation
To render HTML email templates with variables reliably, the main goal is to avoid injecting content in ways that change the email’s table-based layout or CSS rules.
Best practices
1. Use a real template engine
Use something like:
- Handlebars / Mustache
- Liquid
- Jinja2
- EJS
- Twig
These let you safely substitute variables like:
<p>Hello {{ first_name }}</p>
2. Escape dynamic content by default
Email layouts break easily when user data contains:
</>- quotes
- long strings
- line breaks
Always HTML-escape variables unless you explicitly need raw HTML.
Example:
- Safe:
{{ first_name }} - Risky:
{{{ first_name }}}or equivalent “raw” output
If a field is user-generated, escape it.
3. Keep layout in tables
Most email clients still render best with:
<table><tr><td>
Avoid relying on:
- flexbox
- grid
- advanced positioning
- complex external CSS
Use inline styles instead of classes when possible.
4. Sanitize any rich text
If you allow users to insert formatted content like comments or signatures:
- sanitize HTML
- strip scripts, iframes, and unsupported tags
- whitelist only safe elements
Never inject unsanitized HTML into an email.
5. Be careful with conditional blocks
Template logic is useful, but don’t let it change the structure unpredictably.
Good:
{% if promo_code %}
<p>Your code: {{ promo_code }}</p>
{% endif %}
Risky:
- inserting entire rows/cells without maintaining table structure
- leaving empty
<td>s or broken tags
6. Test with real email clients
Different clients render differently:
- Gmail
- Outlook
- Apple Mail
- Yahoo Mail
Preview/test in multiple clients before sending. Tools like:
- Litmus
- Email on Acid
- Mailtrap
- built-in provider previews
7. Keep variable content constrained
If a variable can be very long, wrap or truncate it:
- names
- addresses
- product descriptions
- URLs
Example:
<td style="word-break: break-word;">
{{ long_text }}
</td>
8. Use fallback values
Avoid blank spots that shift the layout.
<p>Hello {{ first_name or "there" }}</p>
9. Inline CSS after rendering
If you use a build pipeline:
- render template variables
- inline CSS
- send email
Many systems use a premailer-style step to convert CSS into inline styles.
Simple example
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px;">
<tr>
<td style="font-family: Arial, sans-serif; font-size: 16px; line-height: 24px;">
<p>Hello {{ first_name }},</p>
<p>Your order <strong>#{{ order_id }}</strong> has shipped.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
Use escaping so if first_name = "<script>", it renders as text, not code.
Rule of thumb
If the variable is:
- plain text → escape it
- HTML you trust → sanitize and insert carefully
- optional → provide a fallback
- long → constrain/wrap it
If you want, I can also show:
- a safe template example in Jinja2/Handlebars
- how to inline CSS for email
- a recommended email-safe HTML skeleton