Upstream MJML supports placing raw content before the generated <!doctype html> declaration:
<mjml>
<mj-raw position="file-start">
<!-- generated by our mail template system -->
</mj-raw>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello!</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
Expected output:
<!-- generated by our mail template system -->
<!doctype html>
<html ...>
mjml-python currently drops the root-level mj-raw element and starts the output directly with the doctype.
Example use case
Some template and email systems store metadata at the start of the template. For example, a Twig/Craft CMS template may contain routing information consumed before the HTML is sent:
<mjml>
<mj-raw position="file-start">
{# subject: Password reset for {{ user.email }} #}
{# fromEmail: {{ systemEmail }} #}
{# fromName: Support #}
{# includeAttachments: false #}
</mj-raw>
<mj-body>
...
</mj-body>
</mjml>
This metadata must remain outside the HTML document. Putting it inside <head> or <body> could expose it as document content or interfere with later template processing.
Upstream itself uses Twig-style metadata as a regression-test example for this feature.
Upstream behavior
Upstream collects direct mj-raw children of <mjml> whose position attribute is file-start. Their contents are joined with newlines and stored as globalData.beforeDoctype.
The skeleton then emits:
`${beforeDoctype ? `${beforeDoctype}\n` : ''}<!doctype html>`
Relevant upstream code:
packages/mjml-core/src/index.js, handling of mjOutsideRaws
packages/mjml-core/src/helpers/skeleton.js, beforeDoctype
packages/mjml-raw/src/index.js, declaration of position
packages/mjml-raw/README.md
Proposed implementation
- Add
position: enum(file-start) to MjRaw.allowed_attrs().
- Collect direct
mj-raw children of the root <mjml> element.
- Select elements whose
position is file-start.
- Join their contents with
\n, preserving document order.
- Pass the result to the skeleton as
beforeDoctype.
- Emit one newline between the prefix and the doctype.
- Continue ignoring root-level
mj-raw elements without position="file-start", matching upstream.
- Do not change the behavior of
mj-raw inside mj-head or mj-body.
Acceptance criteria
Given:
<mjml>
<mj-raw position="file-start">first</mj-raw>
<mj-raw position="file-start">second</mj-raw>
<mj-body>
<mj-section>
<mj-column><mj-text>Hello</mj-text></mj-column>
</mj-section>
</mj-body>
</mjml>
the result must start with:
first
second
<!doctype html>
The remainder of the generated document must continue to match upstream.
Add regression coverage for:
- one
file-start element;
- multiple elements and their ordering;
- template metadata containing Twig/Jinja-style syntax;
- an ordinary root-level
mj-raw without position;
- ordinary
mj-raw inside the body.
Testing caveat
The existing assert_same_html() comparison cannot detect a missing HTML comment before the doctype. HTMLCompare parses only the resulting HTML document tree; pre-doctype comments are currently discarded. This remains true even with ignore_comments=False.
The mjml-python regression test must therefore contain a raw-string assertion, for example:
assert actual_html.startswith(
"<!-- generated by our mail template system -->\n<!doctype html>"
)
The remainder of the document can still be checked with assert_same_html().
A useful separate HTMLCompare enhancement would be an opt-in option such as compare_document_prefix=True. It would need to capture the source before the doctype before passing the document to html5lib, then report something like DOCUMENT_PREFIX_MISMATCH. Making it opt-in would preserve HTMLCompare’s current semantic behavior and avoid unexpectedly treating ordinary pre-doctype comments as significant.
Upstream MJML supports placing raw content before the generated
<!doctype html>declaration:Expected output:
mjml-pythoncurrently drops the root-levelmj-rawelement and starts the output directly with the doctype.Example use case
Some template and email systems store metadata at the start of the template. For example, a Twig/Craft CMS template may contain routing information consumed before the HTML is sent:
This metadata must remain outside the HTML document. Putting it inside
<head>or<body>could expose it as document content or interfere with later template processing.Upstream itself uses Twig-style metadata as a regression-test example for this feature.
Upstream behavior
Upstream collects direct
mj-rawchildren of<mjml>whosepositionattribute isfile-start. Their contents are joined with newlines and stored asglobalData.beforeDoctype.The skeleton then emits:
`${beforeDoctype ? `${beforeDoctype}\n` : ''}<!doctype html>`Relevant upstream code:
packages/mjml-core/src/index.js, handling ofmjOutsideRawspackages/mjml-core/src/helpers/skeleton.js,beforeDoctypepackages/mjml-raw/src/index.js, declaration ofpositionpackages/mjml-raw/README.mdProposed implementation
position: enum(file-start)toMjRaw.allowed_attrs().mj-rawchildren of the root<mjml>element.positionisfile-start.\n, preserving document order.beforeDoctype.mj-rawelements withoutposition="file-start", matching upstream.mj-rawinsidemj-headormj-body.Acceptance criteria
Given:
the result must start with:
The remainder of the generated document must continue to match upstream.
Add regression coverage for:
file-startelement;mj-rawwithoutposition;mj-rawinside the body.Testing caveat
The existing
assert_same_html()comparison cannot detect a missing HTML comment before the doctype. HTMLCompare parses only the resulting HTML document tree; pre-doctype comments are currently discarded. This remains true even withignore_comments=False.The mjml-python regression test must therefore contain a raw-string assertion, for example:
The remainder of the document can still be checked with
assert_same_html().A useful separate HTMLCompare enhancement would be an opt-in option such as
compare_document_prefix=True. It would need to capture the source before the doctype before passing the document tohtml5lib, then report something likeDOCUMENT_PREFIX_MISMATCH. Making it opt-in would preserve HTMLCompare’s current semantic behavior and avoid unexpectedly treating ordinary pre-doctype comments as significant.