A native Go compiler for MJML emails, maintained by Notifuse.
gomjml turns MJML markup into responsive, email-client-safe HTML from Go code, with no Node.js process to run. It implements the MJML body components and the main head components, and aims for output identical to the official MJML implementation.
Notifuse is an open-source, self-hosted platform for newsletters, email marketing and transactional emails, built with Go and React. Its drag-and-drop email builder produces MJML, and gomjml compiles that MJML into the HTML that is actually sent.
Every email sent from Notifuse is rendered by this library, so bugs found in real templates are fixed here first, released as tagged versions, then picked up by Notifuse.
- Notifuse Cloud: notifuse.com
- Source code: github.com/Notifuse/notifuse
- Live demo: demo.notifuse.com
go get github.com/Notifuse/gomjmlpackage main
import (
"fmt"
"log"
"github.com/Notifuse/gomjml/mjml"
)
func main() {
html, err := mjml.Render(`<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello World!</mj-text>
<mj-button href="https://example.com">Click Me</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>`)
if err != nil {
log.Fatal(err)
}
fmt.Println(html)
}go install github.com/Notifuse/gomjml/cmd/gomjml@latestgomjml compile input.mjml -o output.html # write to a file
gomjml compile input.mjml -s # write to stdout
gomjml compile input.mjml -s --debug # add data-mj-debug-* attributes
gomjml compile input.mjml -o output.html --cache --cache-ttl=10m| Flag | Description |
|---|---|
-o, --output |
Output file path |
-s, --stdout |
Write to stdout |
--debug |
Include debug attributes, to trace HTML back to MJML components |
--cache |
Enable AST caching |
--cache-ttl |
Cache TTL (default 5m) |
--cache-cleanup-interval |
Cache cleanup interval (default: TTL / 2) |
--strict |
Exit with status 1 when there are validation warnings (always printed to stderr) |
// Debug attributes
html, err := mjml.Render(template, mjml.WithDebugTags(true))
// AST cache: reuses the parsed template when the same MJML is rendered again
html, err := mjml.Render(template, mjml.WithCache())The cache is opt-in. It helps when the same templates are rendered many times, as in a server sending the same email to many recipients; it does not help for one-off renders or short-lived processes.
// Configure once, before the first cached render
mjml.SetASTCacheTTLOnce(10 * time.Minute)
mjml.SetASTCacheCleanupIntervalOnce(5 * time.Minute)
// In long-running applications, stop the cleanup goroutine on shutdown
defer mjml.StopASTCacheCleanup()- Safe for concurrent use; concurrent renders of the same new template parse it only once.
- Entries expire after the TTL (default 5 minutes) and are removed by a background cleanup.
- There is no size limit: each cached template takes roughly 5–50 KB, so watch memory if you render many distinct templates.
Like MJML with its default soft validation, gomjml renders documents with unknown tags, misplaced components or invalid attributes, and reports them as warnings instead of errors:
result, err := mjml.RenderWithAST(template)
for _, w := range result.Warnings {
fmt.Printf("line %d (%s): %s\n", w.Line, w.TagName, w.Message)
// line 12 (mj-text): Attribute foo is illegal
}
// Or, with any entry point:
html, err := mjml.Render(template, mjml.WithWarningHandler(func(w mjml.Warning) {
log.Printf("%s: %s", w.Kind, w.Message)
}))Messages follow MJML's validator. Kind is one of unknown-tag, unsupported-tag (an MJML tag gomjml does not implement), misplaced-tag or invalid-attribute. Unknown and unsupported tags are left out; misplaced components are rendered by their parent (directly under <mjml>, only mj-head and mj-body are used); invalid attributes are ignored. Warnings are reported in document order.
ast, err := parser.ParseMJML(template)
if err != nil {
return err
}
component, err := mjml.NewFromAST(ast)
if err != nil {
return err
}
html, err := mjml.RenderComponentString(component)gomjml targets the output of MJML 4.18. The test suite compares gomjml's output with reference HTML in mjml/testdata.
Implemented components:
- Layout:
mjml,mj-body,mj-wrapper,mj-section,mj-group,mj-column - Content:
mj-text,mj-button,mj-image,mj-divider,mj-spacer,mj-table,mj-raw,mj-hero - Navigation and social:
mj-navbar,mj-navbar-link,mj-social,mj-social-element - Interactive:
mj-accordion,mj-accordion-element,mj-accordion-title,mj-accordion-text,mj-carousel,mj-carousel-image - Head:
mj-head,mj-title,mj-preview,mj-font,mj-style,mj-breakpoint,mj-attributes(withmj-allandmj-class)
Not implemented yet: mj-html-attributes and mj-include.
If gomjml's output differs from MJML's for a template, please open an issue with the MJML that shows it.
On the Austin template from mjml.io, compiling 50 times through the CLI takes about 160 ms with gomjml, against about 13.5 s with the JavaScript MJML CLI, using 2 MB of memory instead of 90 MB. See docs/benchmarks.md for the method, the full results and the Go benchmarks.
The generated HTML includes the Outlook conditional comments and VML backgrounds MJML produces, and targets the same clients:
- Microsoft Outlook (desktop, Outlook.com)
- Gmail (web, iOS, Android)
- Apple Mail (macOS, iOS)
- Yahoo Mail, Thunderbird
This repository started as a fork of preslavrachev/gomjml, created by Preslav Rachev, and is now developed independently by Notifuse so that bug fixes can be released without waiting on upstream.
Since v0.14.0 the module path is github.com/Notifuse/gomjml; earlier tags of this repository still declare the original path. To migrate from github.com/preslavrachev/gomjml:
go get github.com/Notifuse/gomjml@latestThen replace github.com/preslavrachev/gomjml with github.com/Notifuse/gomjml in your imports, and remove any replace github.com/preslavrachev/gomjml => ... directive from your go.mod. Package names and the API are unchanged.
Issues and pull requests are welcome on Notifuse/gomjml.
go build -o bin/gomjml ./cmd/gomjml # build the CLI
go test ./... # run all tests
go test -race ./... # with the race detectorEach integration test is a pair of files in mjml/testdata: name.mjml and its reference output name.html. To see how gomjml's output differs from the reference for one of them:
go build -o bin/htmlcompare ./cmd/htmlcompare
./bin/htmlcompare mj-button-align --testdata-dir mjml/testdataSee AGENTS.md for the code conventions.
MIT. Copyright (c) 2025 Preslav Rachev; see LICENSE. Modifications maintained by Notifuse.