Skip to content
NotifusePublic

About

A native Go implementation of the MJML email framework, providing fast compilation of MJML markup to responsive HTML.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

gomjml

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.

Tests MJML License

📬 Built for Notifuse

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.

🚀 Quick start

go get github.com/Notifuse/gomjml
package 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)
}

💻 CLI

go install github.com/Notifuse/gomjml/cmd/gomjml@latest
gomjml 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)

📚 Library API

Render options

// 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())

AST cache

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.

Validation warnings

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.

Step-by-step rendering

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)

✅ MJML compatibility

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 (with mj-all and mj-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.

⚡ Performance

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.

📧 Email clients

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

🍴 Fork and migration

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@latest

Then 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.

🤝 Contributing

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 detector

Each 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/testdata

See AGENTS.md for the code conventions.

📄 License

MIT. Copyright (c) 2025 Preslav Rachev; see LICENSE. Modifications maintained by Notifuse.

About

A native Go implementation of the MJML email framework, providing fast compilation of MJML markup to responsive HTML.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages