Skip to main content

django-mjml-templates

A drop-in replacement for Django's template engine that also understands .mjml files, so an email template is an ordinary Django template that happens to compile to email HTML. MJML is compiled with mjml-python, the Rust mrml port, so there is no Node dependency; the plain-text fallback uses html2text.

Setup

uv add django-mjml-templates   # or pip install

Then swap the backend; everything else about TEMPLATES stays as it was. The engine is still named django (Django takes the default name from the module segment of BACKEND), so engines["django"] and using="django" keep working.

TEMPLATES = [{
    "BACKEND": "mjml_templates.django.DjangoTemplates",   # was django.template.backends.django.DjangoTemplates
    "APP_DIRS": True,
    "OPTIONS": {"context_processors": [...]},
}]

Every other template renders exactly as before. Only a template whose name ends in .mjml — or a from_string whose code starts with <mjml — is treated as email.

Writing an email

An .mjml file is a Django template first: {% extends %}, loops, {% url %} and autoescaping all run as usual, and the MJML that produces is compiled on render. See the MJML docs for the components themselves.

{# templates/email/welcome.mjml #}
<mjml>
  <mj-head><mj-title>Welcome, {{ user.first_name }}</mj-title></mj-head>
  <mj-body>
    <mj-section><mj-column>
      <mj-text>Hi {{ user.first_name }}, thanks for signing up.</mj-text>
      <mj-button href="{% url 'start' %}">Get started</mj-button>
    </mj-column></mj-section>
  </mj-body>
</mjml>

<mj-title> doubles as the subject line. A sibling email/welcome.txt, if you write one, becomes the plain-text body; without it the HTML is run through html2text, with MJML's layout tables unwrapped first (an <mj-table> stays a table) and each button written as Label: url.

Sending

from django.template.loader import get_template

get_template("email/welcome.mjml").render_email({"user": user}, request, to=[user.email]).send()

render_email(context, request=None, *, subject="", **kwargs) returns an unsent EmailMultiAlternatives with both bodies attached, so you can still attach() a file first. kwargs go straight to EmailMultiAlternatives: to, from_email, bcc, reply_to, headers, and so on.

render_email_parts(context, request=None, *, subject="") gives the raw (subject, text, html) named tuple instead. Both methods exist on .mjml templates only. The subject is the one you pass, else the rendered <mj-title>, else a ValueError.

Absolute URLs

A relative link is dead in an email — there is no page for the mail client to resolve it against. So every .mjml render rewrites href, src, background and CSS url() against a base URL, and {% url %}, {% static %} and hand-written paths all come out absolute. Anything already carrying a scheme, a protocol-relative //host/… or a bare #fragment is left alone.

The base comes from the request if you pass one, and otherwise from MJML_BASE_URL:

MJML_BASE_URL = "https://example.com"   # a full URL, used as-is
MJML_BASE_URL = "https://"              # scheme only (the default) — paired with the current Site

The default means projects with django.contrib.sites installed need no setting at all. With no request, no full URL and no sites app there is no base to work from, and URLs are left relative rather than raising — so sending from a Celery task or a management command wants one of the two. (A MJML_BASE_URL with no scheme at all is an ImproperlyConfigured.)

The rewrite only reaches URLs in markup. Where a template writes one out as text, and in the .txt sibling where there is no markup at all, use mjml_base:

{# email/welcome.txt #}
Sign up: {{ mjml_base }}{% url "signup" %}

Pass your own mjml_base in the context and it wins over the derived one.

Previewing

preview_response shows an email the way a person meets it: an inbox row, the HTML part and the plain-text part, all read from the message itself. Nothing is wired up for you; your view builds the message, checks who may see it, and returns the response.

from mjml_templates.preview import preview_response

@staff_member_required
def welcome_preview(request):
    message = get_template("email/welcome.mjml").render_email({"user": request.user}, request, to=[request.user.email])
    return preview_response(message)

To embed it in a page of your own, preview_fragment(message) returns the same preview as scoped markup. Tabs, the placeholder inbox, links, attachments, restyling with --mjml-* custom properties and overriding the templates are covered in the preview docs.

Metadata

Release files for django-mjml-templates 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-mjml-templates 1.3.0
File Size Uploaded
django_mjml_templates-1.3.0.tar.gz 46.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-mjml-templates 1.3.0
File Interpreter ABI Platform
django_mjml_templates-1.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 64.8 kB

Release files / django_mjml_templates-1.3.0.tar.gz

Download URL django_mjml_templates-1.3.0.tar.gz
Size 46.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3f49194b0ee6b1cc64bc84717d05847cf0c0699baae4a375e05eea838bd57b8b
BLAKE2b-256 checksum
How to use checksums
2e6e22f544fbe94b28d2439ab85c379341af72f9040b420bc3d4b44362e7c116
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / django_mjml_templates-1.3.0-py3-none-any.whl

Download URL django_mjml_templates-1.3.0-py3-none-any.whl
Size 18.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97d3f3fd1786af112cfc0cf8c9491cdf3b2d8b174edfc54f60029df2a15772de
BLAKE2b-256 checksum
How to use checksums
918de942ef8bc8534a97b04973499bdfa3feb4790d4ea095c456a2a7549a6643
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.0 This release

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page