Email Templates
This project uses MJML for creating responsive, email-client-compatible HTML emails. This document describes how the AMS project uses MJML for email templates, including the shared component system, build pipeline, and best practices.
Overview
The email system follows a build-time compilation approach:
- Design: Email layouts are created using MJML templates
- Build: MJML templates are compiled to HTML at build time
- Runtime: Django renders dynamic data into the compiled HTML templates
- Send: Django sends the final email using the standard email backend
This architecture provides:
- Strong email client compatibility (including Outlook)
- Fast email sending (no runtime compilation)
- Clean separation of concerns
- Easy testing
- No runtime Node.js dependency
Benefits
- Responsive by default: MJML automatically generates responsive HTML emails
- Consistent styling: Shared includes ensure all emails follow the same design system
- Developer-friendly: Cleaner syntax than raw HTML email markup
- Django integration: Templates support Django template tags for dynamic content
Build Pipeline
How It Works
- Source files: Email templates are written as
.mjmlfiles in various app directories - Compilation: The Gulp build process compiles
.mjmlfiles to.htmlfiles - In-place output: HTML files are generated in the same directory as their source
.mjmlfiles - Django integration: Compiled HTML files are loaded by Django's email system
- Git ignored: Compiled
.htmlfiles are ignored by Git (only.mjmlsource files are tracked)
Build Commands
Compile all MJML templates:
docker compose exec node npx gulp mjml
Watch for changes (auto-compile):
docker compose exec node npx gulp dev
Build all assets including MJML:
docker compose exec node npx gulp build
Files Excluded from Compilation
Any MJML file starting with an underscore (_*.mjml) is excluded from compilation. These are include files used by other templates.
Using Shared Includes
Head Attributes
Include shared MJML attributes in your email template's <mj-head> section:
<mjml>
<mj-head>
<mj-title>{% trans "Your Email Title" %}</mj-title>
<mj-preview>{% trans "Preview text here" %}</mj-preview>
<mj-attributes>
<mj-include path="../../_email_includes/_head_attributes.mjml" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f8f9fa">
<!-- Your email content -->
</mj-body>
</mjml>
Head Styles (Optional)
If you need the shared CSS styles:
<mj-head>
<!-- ... -->
<mj-style inline="inline">
<mj-include path="../../_email_includes/_head_styles.mjml" />
</mj-style>
</mj-head>
Standard Footer
For simple emails with a generic footer message:
<mj-body background-color="#f8f9fa">
<!-- Your email content -->
<!-- Footer -->
<mj-include path="../../_email_includes/_footer.mjml" />
</mj-body>
Custom Footers: Many templates use custom footers with specific messaging (e.g., "You're receiving this because you are a staff member") or include action URLs. In these cases, don't use the shared footer—write a custom one instead.
Staff Footer
For staff notification emails:
<mj-body background-color="#f8f9fa">
<!-- Your email content -->
<!-- Footer -->
<mj-include path="../../_email_includes/_staff_footer.mjml" />
</mj-body>
This footer includes a message indicating the recipient is receiving the email because they are a staff member.
Using mj-class for Consistent Styling
The shared _head_attributes.mjml file includes _head_styles.mjml, which defines reusable mj-class declarations. Using these classes ensures consistent styling across all email templates and reduces code verbosity.
Example: Using mj-class
Before (inline styles):
<mj-section background-color="#0056b3" padding="30px 20px">
<mj-column>
<!-- prettier-ignore -->
<mj-text font-size="28px" font-weight="bold" color="#ffffff" align="center">
{% trans "Email Title" %}
</mj-text>
</mj-column>
</mj-section>
After (using mj-class):
<mj-section mj-class="section-header-primary">
<mj-column>
<!-- prettier-ignore -->
<mj-text mj-class="header-primary">
{% trans "Email Title" %}
</mj-text>
</mj-column>
</mj-section>
When NOT to Use mj-class
Some styles cannot use mj-class and must remain inline:
- Table cell styles:
<td style="...">elements cannot usemj-class - Link styles: Styles on
<a>tags within<mj-text>must be inline - One-off custom styles: Unique styles that don't fit a reusable pattern
Example (table styles must stay inline):
<mj-table>
<tr>
<!-- prettier-ignore -->
<td style="padding: 8px 0; font-weight: bold; width: 40%">
{% trans "Label:" %}
</td>
<td style="padding: 8px 0">{{ value }}</td>
</tr>
</mj-table>
Django Template Integration
Template Tags
MJML templates support Django template tags:
<!-- prettier-ignore -->
<mj-text>
{% load i18n %}
{% trans "Translatable text" %}
</mj-text>
<!-- prettier-ignore -->
<mj-text>
{% blocktrans with name=user.name %}
Hello {{ name }}!
{% endblocktrans %}
</mj-text>
Variables
Use Django template variables:
<mj-text>{{ user.email }}</mj-text>
<mj-button href="{{ action_url }}">Click Here</mj-button>
Conditional Content
Use Django conditionals:
<!-- prettier-ignore -->
{% if show_warning %}
<mj-section background-color="#fff3cd" border-left="4px solid #ffc107" padding="15px">
<mj-column>
<mj-text>{% trans "Warning message" %}</mj-text>
</mj-column>
</mj-section>
{% endif %}
Important: For conditionals around MJML tags, use <mj-raw>:
<mj-raw>{% if condition %}</mj-raw>
<mj-section>
<!-- content -->
</mj-section>
<mj-raw>{% endif %}</mj-raw>
Creating a New Email Template
Step 1: Choose Location
Place your template in the appropriate app directory.
Step 2: Create MJML File
Create a .mjml file with this basic structure:
<mjml>
<mj-head>
<mj-title>{{ subject }}</mj-title>
<mj-preview>{{ preview_text }}</mj-preview>
<mj-attributes>
<mj-include path="../../_email_includes/_head_attributes.mjml" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f8f9fa">
<!-- Header -->
<mj-section mj-class="section-header-primary">
<mj-column>
<mj-text mj-class="header-primary">
{% trans "Email Title" %}
</mj-text>
</mj-column>
</mj-section>
<!-- Content -->
<mj-section mj-class="section-content">
<mj-column>
<mj-text mj-class="text-body">
{% trans "Your email content here" %}
</mj-text>
</mj-column>
</mj-section>
<!-- Footer -->
<mj-include path="../../_email_includes/_footer.mjml" />
</mj-body>
</mjml>
Remember: Adjust the include paths based on your template's location.
Step 3: Compile
Compile the MJML to HTML (if it hasn't occurred automatically):
docker compose exec node npx gulp mjml
Step 4: Update .gitignore (if needed)
If you created a template in a new directory, add the compiled HTML pattern to .gitignore:
# Compiled MJML email templates
ams/templates/new_app/emails/*.html
Step 5: Send Email in Django
Use the HTML template in your Django email code when calling send_templated_email() from ams.utils.email.
Best Practices
1. Use Shared Includes
Always use the shared _head_attributes.mjml include for consistent styling across all emails.
2. Follow the Color Palette
Use the defined color palette to maintain visual consistency.
3. Make Text Translatable
Wrap all user-facing text in {% trans %} or {% blocktrans %} tags.
4. Test in Multiple Email Clients
MJML handles most cross-client compatibility, but always test major emails in:
- Gmail
- Outlook
- Apple Mail
- Mobile clients
5. Keep Content Focused
- Use clear, concise subject lines
- Put the most important information first
- Use headers and sections to organize content
- Include clear calls-to-action with buttons
6. Custom Footers When Needed
Don't force-fit the generic footer if your email needs specific footer content (like URLs, specific disclaimers, or context-specific messages).
7. Preview Text
Always include a <mj-preview> tag with a meaningful preview (shown in email client previews).
Troubleshooting
Template Not Compiling
Problem: MJML file isn't generating HTML output
Solutions:
- Check that the file doesn't start with
_(reserved for includes) - Verify the Gulp build process is running
- Check the terminal for MJML compilation errors
- Validate your MJML syntax at MJML Playground
Include Path Errors
Problem: "File not found" errors for includes
Solutions:
- Check the relative path based on your template's location
- Remember:
../../_email_includes/for nested dirs,../_email_includes/for top-level - Ensure
_email_includes/directory exists
Styles Not Applying
Problem: Email doesn't look right
Solutions:
- Ensure you're including
_head_attributes.mjml - Check that the HTML file was regenerated after MJML changes
- Clear email client cache (some clients cache heavily)
Django Tags Not Rendering
Problem: {% trans %} tags showing as literal text
Solutions:
- Ensure you're loading the compiled
.htmlfile, not the.mjmlfile - Check that
{% load i18n %}is present at the top of your template - Verify the Gulp
prependDjangoTags()function is working (should add{% load i18n %}to compiled HTML)
Resources
- MJML Documentation
- MJML Try It Live - Test MJML code in real-time
- MJML Component Reference