A blazingly-fast unofficial port of MJML 4 (by MailJet) to .NET 6 to 8.
MJML
is a markup language created by Mailjet and designed to reduce the pain of coding a responsive email. Its semantic syntax makes the language easy and straightforward while its rich standard components library shortens your development time and lightens your email codebase. MJML’s open-source engine takes care of translating the MJML
you wrote into responsive HTML.
You can find out more about MJML 4 from the official website.
| Official Website | Official Documentation | Official Onboarding |
Firstly, you'll need to reference the MJML.NET
NuGet Package into your project.
PM > Install-Package Mjml.Net
Secondly, include Mjml.NET
namespace into your project.
using Mjml.Net;
Finally, the boilerplate code.
public static void Main (string[] args) {
var mjmlRenderer = new MjmlRenderer();
string text = @"
<mjml>
<mj-head>
<mj-title>Hello World Example</mj-title>
</mj-head>
<mj-body>
<mj-section>
<mj-column>
<mj-text>
Hello World!
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>";
var options = new MjmlOptions
{
Beautify = false
};
var (html, errors) = mjmlRenderer.Render(text, options);
}
MJML is not necessarily valid XML. To allow the XML parser to work properly, the MJML string needs to be sanitized before you render it. You can use the method FixXML
for that. If you store the MJML text in the database, it is store to keep the original version and the sanitized version. With version 2.0 we moved to html parser that follows the official standards and can therefore accept any kind of HTML input. More about this under "Known Issues".
public static void Main (string[] args) {
var mjmlRenderer = new MjmlRenderer();
string text = @"
<mjml>
<mj-head>
<mj-title>Hello World Example</mj-title>
</mj-head>
<mj-body>
<mj-section>
<mj-column>
<mj-text>
Hello World!
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>";
var options = new MjmlOptions
{
Beautify = false
};
var xml = mjmlRenderer.FixXML(text, options);
var (html, errors) = mjmlRenderer.Render(xml, options).Html;
}
You can also specify options to the MJML parser.
Name | Data Type | Default | Description |
---|---|---|---|
Beautify | bool | true | True to beatify the HTML. Impacts performance (slower). |
Breakpoint | string | 480px | The default breakpoint to switch to mobile view. |
FileLoader | IFileLoader? | null | The file path loader for <mj-include path="..." type="..."> which handles loading the files from the specified path attribute. For example, SqlDatabaseFileLoader, InMemoryFileLoader, DiskFileLoader, etc... |
Fonts | IReadOnlyDictionary<string, Font[]> | Preset | A list of supported default fonts. |
ForceOWAQueries | bool | false | True to enable media queries for OWA. |
IdGenerator | IIdGenerator | Preset | The ID generator to create random values for attributes like Ids. |
KeepComments | bool | false | True to keep comments. |
Lax | bool | false | In lax mode some errors in the XML will be fixed. Only work when the MJML is passed in as string. Do not turn this on in production, because it can hurt performance. |
Minify | bool | false | True to minify the HTML. (deprecated with version 2.0.0) |
PostProcessors | IPostProcessor[]? | [] | The post processors that will process the HTML, after rendering. |
Styles | Style[]? | [] | A list of custom styles. |
ValidatorFactory | IValidatorFactory? | null | The current validator, which also defines the validation mode. |
XmlEntities | IReadOnlyDictionary<string, string> | Preset | A list of supported XML entities (deprecated with version 1.16.0) |
MJML.NET
tries to implement all functionality 1-2-1
with the MJML 4 project. However, due to JavaScript not being a typed language this means there has been considerate refactoring to the code to make it more aligned with C# typed requirements.
Type | Component | Implemented | Tests | State |
---|---|---|---|---|
Core | mjml | :white_check_mark: | :white_check_mark: | Feature Complete |
Core | mj-head | :white_check_mark: | :white_check_mark: | Feature Complete |
Core | mj-body | :white_check_mark: | :white_check_mark: | Feature Complete |
Core | mj-include | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-attributes | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-class |
:white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-all |
:white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-breakpoint | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-font | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-html-attributes | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-preview | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-style | :white_check_mark: | :white_check_mark: | Feature Complete |
Head | mj-title | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-accordion | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-button | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-carousel | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-column | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-divider | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-group | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-hero | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-image | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-navbar | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-raw | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-section | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-social | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-spacer | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-table | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-text | :white_check_mark: | :white_check_mark: | Feature Complete |
Body | mj-wrapper | :white_check_mark: | :white_check_mark: | Feature Complete |
MJML supports inline styles (see: https://documentation.mjml.io/#mj-style) and html attributes (see: https://documentation.mjml.io/#mj-html-attributes). These are an expensive feature and can only be done after rendering. Therefore we have introduced a new post processing step, that applies inline styles and attributes using the AngleSharp library. To reduce the dependencies this has been moved to a separate nuget package.
PM > Install-Package Mjml.Net.PostProcessors
You have to add the postprocessor to the options:
var options = new MjmlOptions
{
PostProcessors = [AngleSharpPostProcessor.Default]
};
var mjmlRenderer = new MjmlRenderer();
mjmlRenderer.AddHtmlAttributes();
var (html, errors) = await mjmlRenderer.RenderAsync(text, options);
Post processors can use async methods. Therefore we have introduced new methods to the mjml renderer. Only the asynchronous method variants will run post processors.
Performance was one of the key focuses for this project. We're aiming to support high througput while mainintaing low memory footprint. Below are the benchmarks for every public MJML template compiled (beautified and minified).
Important: These tests do not include loading or saving the template from/to disk.
If you'd like to run the benchmarks your self then you can run the Mjml.Net.Benchmarks
project in release
mode.
> .\Mjml.Net.Benchmark.exe
> .\Mjml.Net.Benchmark.exe -p -i 100
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1586 (21H1/May2021Update)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.101
[Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
Job-YRBKPS : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
Jit=RyuJit Platform=X64 IterationCount=100
MjmlTemplateFilePath : Value of the 'MjmlTemplateFilePath' parameter
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Median : Value separating the higher half of all measurements (50th percentile)
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 us : 1 Microsecond (0.000001 sec)
All times are in μs (1ms = 1000 μs)
Method | MjmlTemplateFilePath | Mean | Error | StdDev | Median | Allocated |
---|---|---|---|---|---|---|
Beautified | Amario.mjml | 924.9 | 7.24 | 20.42 | 920.8 | 810 KB |
Minified | Amario.mjml | 980.1 | 25.57 | 75.39 | 974.1 | 810 KB |
Beautified | Arturia.mjml | 438.2 | 9.29 | 27.38 | 438.9 | 384 KB |
Minified | Arturia.mjml | 430.0 | 8.62 | 25.28 | 430.9 | 384 KB |
Beautified | Austin.mjml | 762.0 | 13.06 | 38.30 | 759.7 | 615 KB |
Minified | Austin.mjml | 756.0 | 13.42 | 39.16 | 756.3 | 615 KB |
Beautified | BlackFriday.mjml | 188.3 | 4.39 | 12.94 | 189.2 | 186 KB |
Minified | BlackFriday.mjml | 194.7 | 4.99 | 14.31 | 197.0 | 186 KB |
Beautified | Card.mjml | 290.3 | 7.10 | 20.04 | 290.0 | 259 KB |
Minified | Card.mjml | 284.3 | 5.82 | 17.17 | 287.0 | 259 KB |
Beautified | Christmas.mjml | 413.5 | 8.55 | 25.09 | 417.3 | 360 KB |
Minified | Christmas.mjml | 401.5 | 7.19 | 21.20 | 403.1 | 360 KB |
Beautified | HappyNewYear.mjml | 156.3 | 3.30 | 9.72 | 156.4 | 166 KB |
Minified | HappyNewYear.mjml | 155.2 | 3.46 | 10.21 | 155.2 | 166 KB |
Beautified | ManyHeroes.mjml | 705.7 | 13.98 | 40.33 | 700.0 | 830 KB |
Minified | ManyHeroes.mjml | 667.6 | 9.15 | 26.41 | 653.7 | 830 KB |
Beautified | OnePage.mjml | 414.9 | 8.47 | 24.58 | 403.5 | 399 KB |
Minified | OnePage.mjml | 424.5 | 9.15 | 26.83 | 416.4 | 399 KB |
Beautified | Proof.mjml | 184.2 | 2.90 | 8.33 | 181.6 | 213 KB |
Minified | Proof.mjml | 189.0 | 3.53 | 9.90 | 186.8 | 213 KB |
Beautified | Racoon.mjml | 928.5 | 8.38 | 23.78 | 921.1 | 810 KB |
Minified | Racoon.mjml | 926.7 | 11.90 | 32.98 | 912.9 | 810 KB |
Beautified | Reactivation.mjml | 261.1 | 4.12 | 12.07 | 257.7 | 259 KB |
Minified | Reactivation.mjml | 263.5 | 5.63 | 16.34 | 260.6 | 259 KB |
Beautified | RealEstate.mjml | 707.6 | 11.65 | 33.43 | 701.2 | 623 KB |
Minified | RealEstate.mjml | 725.4 | 20.80 | 58.66 | 708.5 | 623 KB |
Beautified | Recast.mjml | 674.6 | 7.13 | 20.12 | 666.0 | 619 KB |
Minified | Recast.mjml | 661.1 | 3.86 | 10.63 | 659.8 | 619 KB |
Beautified | Receipt.mjml | 275.5 | 2.64 | 7.26 | 272.8 | 263 KB |
Minified | Receipt.mjml | 274.5 | 1.93 | 5.50 | 272.8 | 263 KB |
Beautified | Referral.mjml | 135.3 | 0.75 | 2.19 | 134.9 | 136 KB |
Minified | Referral.mjml | 133.4 | 0.86 | 2.43 | 132.8 | 136 KB |
Beautified | SpheroDroids.mjml | 376.4 | 2.28 | 6.43 | 374.7 | 351 KB |
Minified | SpheroDroids.mjml | 374.5 | 3.66 | 10.01 | 371.8 | 351 KB |
Beautified | SpheroMini.mjml | 395.9 | 4.43 | 12.79 | 390.4 | 368 KB |
Minified | SpheroMini.mjml | 395.4 | 2.11 | 6.13 | 394.3 | 368 KB |
Beautified | UGGRoyale.mjml | 1,126.0 | 6.03 | 17.09 | 1,122.9 | 1,004 KB |
Minified | UGGRoyale.mjml | 1,154.0 | 10.93 | 31.89 | 1,142.2 | 1,004 KB |
Beautified | Welcome.mjml | 125.1 | 0.89 | 2.49 | 124.1 | 131 KB |
Minified | Welcome.mjml | 126.3 | 1.51 | 4.42 | 125.3 | 131 KB |
Beautified | Worldly.mjml | 637.6 | 3.21 | 9.22 | 634.9 | 572 KB |
Minified | Worldly.mjml | 629.6 | 4.24 | 11.60 | 627.9 | 572 KB |
We are aware with an issue where by using HTML Character Entities (e.g. ©
) are unknown and throw an unhandled exception during the rendering of the MJML document. This is because we use XmlReader as the main driver for parsing the MJML document.
The solution is to change the HTML Character Entity Names (e.g. ©
) to there corresponding HTML Character Entity Number (e.g. ©
) in the MJML document.
Here are some of the common HTML Character Entities:
Result | Description | Entity Name | Entity Number |
---|---|---|---|
non-breaking space | |
  |
|
< | less than | < |
< |
> | greater than | > |
> |
& | ampersand | & |
& |
" | double quotation mark | " |
" |
' | single quotation mark (apostrophe) | ' |
' |
© | copyright | © |
© |
® | registered trademark | ® |
® |
™ | registered trademark | ™ |
™ |
We are aware of an issue with non-encoded URL's being recognized as character entities leading to an exception. This is because we use XmlReader as the main driver for parsing the MJML document. This solution is to URL encode all of the URLs in the template or turn on the lax mode with options.Lax = true
.
Some HTML tags are not valid HTML. Especially line break (<br>
). Convert these tags to XHTML (<br></br>
or <br />
) or turn on the lax mode with options.Lax = true
.
We really appreciate any contribution to the project to help provide a native version of MJML to C#. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Once again, it's good to share some appreciation to the projects that make MJML.NET
possible.