Boostport/mjml-go

Compile MJML to HTML directly in your Go applications!

Go

121

61 commits

updated Aug 15, 2025

See the code

README

mjml-go

Go Reference Tests Status Code Coverage

Compile MJML into HTML directly in your Go application!

Why?

MJML is a JavaScript library. In order to use it with other languages, the usual approach is to wrap the library in a Node.js HTTP server and provide an endpoint through which applications not written in JavaScript can make HTTP requests to compile MJML into HTML.

This approach poses some challenges, for example, if MJML is upgraded to a new major version in the deployed Node.js servers, applications calling these servers will need to be upgraded in a synchronized manner to avoid incompatibilities. In addition, running these extra servers introduces extra moving parts and the network into the mix.

This is why we built mjml-go and created an idiomatic Go API to compile MJML into HTML directly in Go applications that can be deployed as a single Go binary.

How?

We wrote a simple JavaScript wrapper that wraps around the MJML library by accepting input and returning output using JSON. This wrapper is then bundled using webpack and compiled into a WebAssembly module using Suborbital's Javy fork, a Javascript to WebAssembly compiler. The WebAssembly module is then compressed using Brotli to yield a 10x reduction in file size.

During runtime, the module is decompressed and loaded into a Wazero runtime on application start up to accept input in order to compile MJML into HTML.

Workers

As WebAssembly modules compiled using Javy are not thread-safe and cannot be called concurrently, the library maintains a pool of 1 to 10 instances to perform compilations. Idle instances are automatically destroyed and will be re-created when they are needed. This means that the library is thread-safe and you can use it concurrently in multiple goroutines.

Example

func main() {
	
	input := `<mjml><mj-body><mj-section><mj-column><mj-divider border-color="#F45E43"></mj-divider><mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text></mj-column></mj-section></mj-body></mjml>`
	
	output, err := mjml.ToHTML(context.Background(), input, mjml.WithMinify(true))
	
	var mjmlError mjml.Error
	
	if errors.As(err, &mjmlError){
	    fmt.Println(mjmlError.Message)
	    fmt.Println(mjmlError.Details)	
	}
	
	fmt.Println(output)
}

Options

The library provides a complete list of options to customize the MJML compilation process including options for html-minifier, js-beautify and juice.

These are all exposed via an idiomatic Go API and a complete list can be found in the Go documentation.

Defaults

If beautify and minify are enabled, but no options were passed in, the library defaults to using the same defaults as the MJML CLI application:

For minify:

optionvalue
CaseSensitivetrue
CollapseWhitespacetrue
MinifyCSSfalse
RemoveEmptyAttributestrue

For beautify:

optionvalue
EndWithNewlinetrue
IndentSize2
PreserveNewlinesfalse
WrapAttributesIndentSize2

Limitations

The WebAssembly module is not able to access the filesystem, so <mj-include> tags are ignored. The solution is to flatten your templates during development and pass the flattened templates to mjml.ToHTML().

This example provides a good starting point to create a Node.js script to do this:

import mjml2html from 'mjml' // load default component
import components from 'mjml-core/lib/components.js'
import Parser from 'mjml-parser-xml'
import jsonToXML from 'mjml-core/lib/helpers/jsonToXML.js'

const xml = `<mjml>...</mjml>`

const mjml = Parser(xml, {
      components,
      filePath: '.',
      actualPath: '.'
    })

console.log(JSON.stringify(mjml))
console.log(jsonToXML(mjml))

Differences from the MJML JavaScript library

  • Beautify and minify will be removed from the library in MJML5 and will be moved into the MJML CLI. Therefore, to prepare for this move, the wrapper imports html-minifier and js-beautify directly to support minifying and beautifying the output.
  • In the current implementation of mjml, it is not possible to customize the output of js-beautify. In this library, we have exposed those options.

Benchmarks

We are benchmarking against a very minimal Node.js server serving a single API endpoint.

goos: linux
goarch: amd64
pkg: github.com/Boostport/mjml-go
cpu: 12th Gen Intel(R) Core(TM) i7-12700F
BenchmarkNodeJS/black-friday-20                      524           2137932 ns/op
BenchmarkNodeJS/one-page-20                          246           4574134 ns/op
BenchmarkNodeJS/reactivation-email-20                180           6386099 ns/op
BenchmarkNodeJS/real-estate-20                       144           8674627 ns/op
BenchmarkNodeJS/recast-20                            154           7289670 ns/op
BenchmarkNodeJS/receipt-email-20                     295           4112735 ns/op
BenchmarkMJMLGo/black-friday-20                       31          35718282 ns/op
BenchmarkMJMLGo/one-page-20                           14          84422520 ns/op
BenchmarkMJMLGo/reactivation-email-20                 15          73484300 ns/op
BenchmarkMJMLGo/real-estate-20                         7         170549075 ns/op
BenchmarkMJMLGo/recast-20                              8         139803444 ns/op
BenchmarkMJMLGo/receipt-email-20                      15          71913776 ns/op
PASS
ok      github.com/Boostport/mjml-go    27.786s

In its current state the Node.js implementation is significantly faster than mjml-go. However, with improvements to Wazero (in particular tetratelabs/wazero#618 and tetratelabs/wazero#179), module instantiation times should see great improvement, reducing worker spin-up times and improving the compilation performance.

Also, we should see improvements from Javy improve these numbers as well.

Development

Run tests

You can run tests using docker by running docker compose run test from the root of the repository.

Run benchmarks

From the root of the repository, run go test -bench=. ./.... Alternatively, you can run them in a docker container: docker compose run benchmark

Compile WebAssembly module and build Node.js test server

Run docker compose run build-js from the root of the repository.

Other languages

Since the MJML library is compiled into a WebAssembly module, it should be relatively easy to take the compiled module and drop it into languages with WebAssembly environments.

If you've created a library for another language, please let us know, so that we can add it to this list!

go
golang
mjml
mjml-to-html
webassembly

Contributors

F21

52 commits

fes300

2 commits

Boostport/mjml-go

Compile MJML to HTML directly in your Go applications!

Go

121

61 commits

updated Aug 15, 2025

See the code

README

mjml-go

Go Reference Tests Status Code Coverage

Compile MJML into HTML directly in your Go application!

Why?

MJML is a JavaScript library. In order to use it with other languages, the usual approach is to wrap the library in a Node.js HTTP server and provide an endpoint through which applications not written in JavaScript can make HTTP requests to compile MJML into HTML.

This approach poses some challenges, for example, if MJML is upgraded to a new major version in the deployed Node.js servers, applications calling these servers will need to be upgraded in a synchronized manner to avoid incompatibilities. In addition, running these extra servers introduces extra moving parts and the network into the mix.

This is why we built mjml-go and created an idiomatic Go API to compile MJML into HTML directly in Go applications that can be deployed as a single Go binary.

How?

We wrote a simple JavaScript wrapper that wraps around the MJML library by accepting input and returning output using JSON. This wrapper is then bundled using webpack and compiled into a WebAssembly module using Suborbital's Javy fork, a Javascript to WebAssembly compiler. The WebAssembly module is then compressed using Brotli to yield a 10x reduction in file size.

During runtime, the module is decompressed and loaded into a Wazero runtime on application start up to accept input in order to compile MJML into HTML.

Workers

As WebAssembly modules compiled using Javy are not thread-safe and cannot be called concurrently, the library maintains a pool of 1 to 10 instances to perform compilations. Idle instances are automatically destroyed and will be re-created when they are needed. This means that the library is thread-safe and you can use it concurrently in multiple goroutines.

Example

func main() {
	
	input := `<mjml><mj-body><mj-section><mj-column><mj-divider border-color="#F45E43"></mj-divider><mj-text font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text></mj-column></mj-section></mj-body></mjml>`
	
	output, err := mjml.ToHTML(context.Background(), input, mjml.WithMinify(true))
	
	var mjmlError mjml.Error
	
	if errors.As(err, &mjmlError){
	    fmt.Println(mjmlError.Message)
	    fmt.Println(mjmlError.Details)	
	}
	
	fmt.Println(output)
}

Options

The library provides a complete list of options to customize the MJML compilation process including options for html-minifier, js-beautify and juice.

These are all exposed via an idiomatic Go API and a complete list can be found in the Go documentation.

Defaults

If beautify and minify are enabled, but no options were passed in, the library defaults to using the same defaults as the MJML CLI application:

For minify:

optionvalue
CaseSensitivetrue
CollapseWhitespacetrue
MinifyCSSfalse
RemoveEmptyAttributestrue

For beautify:

optionvalue
EndWithNewlinetrue
IndentSize2
PreserveNewlinesfalse
WrapAttributesIndentSize2

Limitations

The WebAssembly module is not able to access the filesystem, so <mj-include> tags are ignored. The solution is to flatten your templates during development and pass the flattened templates to mjml.ToHTML().

This example provides a good starting point to create a Node.js script to do this:

import mjml2html from 'mjml' // load default component
import components from 'mjml-core/lib/components.js'
import Parser from 'mjml-parser-xml'
import jsonToXML from 'mjml-core/lib/helpers/jsonToXML.js'

const xml = `<mjml>...</mjml>`

const mjml = Parser(xml, {
      components,
      filePath: '.',
      actualPath: '.'
    })

console.log(JSON.stringify(mjml))
console.log(jsonToXML(mjml))

Differences from the MJML JavaScript library

  • Beautify and minify will be removed from the library in MJML5 and will be moved into the MJML CLI. Therefore, to prepare for this move, the wrapper imports html-minifier and js-beautify directly to support minifying and beautifying the output.
  • In the current implementation of mjml, it is not possible to customize the output of js-beautify. In this library, we have exposed those options.

Benchmarks

We are benchmarking against a very minimal Node.js server serving a single API endpoint.

goos: linux
goarch: amd64
pkg: github.com/Boostport/mjml-go
cpu: 12th Gen Intel(R) Core(TM) i7-12700F
BenchmarkNodeJS/black-friday-20                      524           2137932 ns/op
BenchmarkNodeJS/one-page-20                          246           4574134 ns/op
BenchmarkNodeJS/reactivation-email-20                180           6386099 ns/op
BenchmarkNodeJS/real-estate-20                       144           8674627 ns/op
BenchmarkNodeJS/recast-20                            154           7289670 ns/op
BenchmarkNodeJS/receipt-email-20                     295           4112735 ns/op
BenchmarkMJMLGo/black-friday-20                       31          35718282 ns/op
BenchmarkMJMLGo/one-page-20                           14          84422520 ns/op
BenchmarkMJMLGo/reactivation-email-20                 15          73484300 ns/op
BenchmarkMJMLGo/real-estate-20                         7         170549075 ns/op
BenchmarkMJMLGo/recast-20                              8         139803444 ns/op
BenchmarkMJMLGo/receipt-email-20                      15          71913776 ns/op
PASS
ok      github.com/Boostport/mjml-go    27.786s

In its current state the Node.js implementation is significantly faster than mjml-go. However, with improvements to Wazero (in particular tetratelabs/wazero#618 and tetratelabs/wazero#179), module instantiation times should see great improvement, reducing worker spin-up times and improving the compilation performance.

Also, we should see improvements from Javy improve these numbers as well.

Development

Run tests

You can run tests using docker by running docker compose run test from the root of the repository.

Run benchmarks

From the root of the repository, run go test -bench=. ./.... Alternatively, you can run them in a docker container: docker compose run benchmark

Compile WebAssembly module and build Node.js test server

Run docker compose run build-js from the root of the repository.

Other languages

Since the MJML library is compiled into a WebAssembly module, it should be relatively easy to take the compiled module and drop it into languages with WebAssembly environments.

If you've created a library for another language, please let us know, so that we can add it to this list!

go
golang
mjml
mjml-to-html
webassembly

Contributors

F21

52 commits

fes300

2 commits

Languages

Go

88.0%

JavaScript

11.1%