A native Go implementation of the MJML email framework, providing fast compilation of MJML markup to responsive HTML. This implementation has been inspired by and tested against MRML, the Rust implementation of MJML. See some performance benchmarks for detailed comparison with other MJML implementations.
Full Disclosure: This project has been created in cooperation with Claude Code. I wouldn't have been able to achieve such a feat without Claude's help in turning my bizarre requirements into Go code. Still, it wasn't all smooth sailing. While Claude was able to generate a plausible MVP relatively quickly, bringing it something even remotely usable took a lot more human guidance, going back and forth, throwing away a bunch of code and starting over. There's lots I have learned in the process, and I will soon write a series of blog posts addressing my experience.
- Complete MJML Implementation: 100% feature-complete with all 26 MJML components implemented and tested against MRML (the Rust implementation of MJML). A well-structured Go library with clean package separation
- Email Compatible: Generates HTML that works across email clients (Outlook, Gmail, Apple Mail, etc.)
- Fast Performance: Native Go performance, comparable to Rust MRML implementation
- Optional AST Caching: Opt-in template caching for speedup on repeated renders
- Complete Component System: Support for essential MJML components with proper inheritance
- CLI & Library: Use as command-line tool or importable Go package
- Tested Against MRML: Integration tests validate output compatibility with reference implementation
# Clone and build
git clone https://github.com/preslavrachev/gomjml
cd gomjml
go build -o bin/gomjml ./cmd/gomjml
# Add to PATH (optional)
export PATH=$PATH:$(pwd)/bin
# Import as library
go get github.com/preslavrachev/gomjml
The CLI provides a structured command system with individual commands:
# Basic compilation
./bin/gomjml compile input.mjml -o output.html
# Output to stdout
./bin/gomjml compile input.mjml -s
# Include debug attributes for component traceability
./bin/gomjml compile input.mjml -s --debug
# Enable caching for better performance on repeated renders
./bin/gomjml compile input.mjml -o output.html --cache
# Configure cache with custom TTL
./bin/gomjml compile input.mjml -o output.html --cache --cache-ttl=10m
# Run test suite
./bin/gomjml test
# Get help
./bin/gomjml --help
./bin/gomjml compile --help
compile [input]
- Compile MJML to HTML (main command)test
- Run test suite against MRML reference implementationhelp
- Show help information
-o, --output string
: Output file path-s, --stdout
: Output to stdout--debug
: Include debug attributes for component traceability (default: false)--cache
: Enable AST caching for performance (default: false)--cache-ttl
: Cache TTL duration (default: 5m)--cache-cleanup-interval
: Cache cleanup interval (default:cache-ttl/2
)
The implementation provides clean, importable packages:
package main
import (
"fmt"
"log"
"github.com/preslavrachev/gomjml/mjml"
"github.com/preslavrachev/gomjml/parser"
)
func main() {
mjmlContent := `<mjml>
<mj-head>
<mj-title>My Newsletter</mj-title>
</mj-head>
<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>`
// Method 1: Direct rendering (recommended)
html, err := mjml.Render(mjmlContent)
if err != nil {
log.Fatal("Render error:", err)
}
fmt.Println(html)
// Method 1b: Direct rendering with debug attributes
htmlWithDebug, err := mjml.Render(mjmlContent, mjml.WithDebugTags(true))
if err != nil {
log.Fatal("Render error:", err)
}
fmt.Println(htmlWithDebug) // Includes data-mj-debug-* attributes
// Method 1c: Enable caching for performance (opt-in feature)
htmlWithCache, err := mjml.Render(mjmlContent, mjml.WithCache())
if err != nil {
log.Fatal("Render error:", err)
}
fmt.Println(htmlWithCache) // Uses cached AST if available
// For long-running applications, configure cache TTL before first use
mjml.SetASTCacheTTLOnce(10 * time.Minute)
// For graceful shutdown in long-running applications (optional)
// Not needed for CLI tools or short-lived processes
defer mjml.StopASTCacheCleanup()
// Method 2: Step-by-step processing
ast, err := parser.ParseMJML(mjmlContent)
if err != nil {
log.Fatal("Parse error:", err)
}
component, err := mjml.NewFromAST(ast)
if err != nil {
log.Fatal("Component creation error:", err)
}
html, err = mjml.RenderComponentString(component)
if err != nil {
log.Fatal("Render error:", err)
}
fmt.Println(html)
}
While it is not recommended to do so, because it will break the compatibility with the MJML specification, you can fork the repository and add new components by following these steps:
// 1. Create component file in mjml/components/
package components
import (
"io"
"strings"
"github.com/preslavrachev/gomjml/mjml/options"
"github.com/preslavrachev/gomjml/parser"
)
type MJNewComponent struct {
*BaseComponent
}
func NewMJNewComponent(node *parser.MJMLNode, opts *options.RenderOpts) *MJNewComponent {
return &MJNewComponent{
BaseComponent: NewBaseComponent(node, opts),
}
}
// Note: RenderString() is no longer part of the Component interface
// Use mjml.RenderComponentString(component) helper function instead
func (c *MJNewComponent) Render(w io.Writer) error {
// Implementation here - write HTML directly to Writer
// Use c.AddDebugAttribute(tag, "new") for debug traceability
// Example implementation:
// if _, err := w.Write([]byte("<div>Hello World</div>")); err != nil {
// return err
// }
return nil
}
func (c *MJNewComponent) GetTagName() string {
return "mj-new"
}
// 2. Add to component factory in mjml/component.go
case "mj-new":
return components.NewMJNewComponent(node, opts), nil
// 3. Add test cases in mjml/integration_test.go
// 4. Update README.md documentation
All MJML components must implement the Component
interface, which requires:
Render(w io.Writer) error
: Primary rendering method that writes HTML directly to a Writer for optimal performanceGetTagName() string
: Returns the component's MJML tag name
For string-based rendering, use the helper function mjml.RenderComponentString(component)
instead of a component method.
If you need to register a component but won't implement its functionality right away, use the NotImplementedError
pattern:
func (c *MJNewComponent) Render(w io.Writer) error {
// TODO: Implement mj-new component functionality
return &NotImplementedError{ComponentName: "mj-new"}
}
func (c *MJNewComponent) GetTagName() string {
return "mj-new"
}
Component | Status | Description |
---|---|---|
Core Layout | ||
mjml |
β Implemented | Root document container with DOCTYPE and HTML structure |
mj-head |
β Implemented | Document metadata container |
mj-body |
β Implemented | Email body container with responsive layout |
mj-section |
β Implemented | Layout sections with background support |
mj-column |
β Implemented | Responsive columns with automatic width calculation |
mj-wrapper |
β Implemented | Wrapper component with border, background-color, and padding support |
mj-group |
β Implemented | Group multiple columns in a section |
Content Components | ||
mj-text |
β Implemented | Text content with full styling support |
mj-button |
β Implemented | Email-safe buttons with customizable styling and links |
mj-image |
β Implemented | Responsive images with link wrapping and alt text |
mj-divider |
β Implemented | Visual separators and spacing elements |
mj-social |
β Implemented | Social media icons container |
mj-social-element |
β Implemented | Individual social media icons |
mj-navbar |
β Implemented | Navigation bar component |
mj-navbar-link |
β Implemented | Navigation links within navbar |
mj-raw |
β Implemented | Raw HTML content insertion |
Head Components | ||
mj-title |
β Implemented | Document title for email clients |
mj-font |
β Implemented | Custom font imports with Google Fonts support |
mj-preview |
β Implemented | Preview text for email clients |
mj-style |
β Implemented | Custom CSS styles |
mj-attributes |
β Implemented | Global attribute definitions |
mj-all |
β Implemented | Global attributes for all components |
Not Implemented | ||
mj-accordion |
β Implemented | Collapsible content sections |
mj-accordion-text |
β Implemented | Text content within accordion |
mj-accordion-title |
β Implemented | Title for accordion sections |
mj-carousel |
β Implemented | Interactive image carousel component |
mj-carousel-image |
β Implemented | Images within carousel |
mj-hero |
β Implemented | Header/banner sections with background images |
mj-spacer |
β Implemented | Layout spacing control |
mj-table |
β Implemented | Email-safe table component with border and styling support |
- β Implemented: 26 components - All essential layout, content, head components, accordion, navbar, hero, spacer, table, and carousel components work
- β Not Implemented: 0 components - Full MJML specification coverage achieved
- Total MJML Components: 26 - Complete coverage of all major MJML specification components
Based on the integration test suite in mjml/integration_test.go
, the implemented components are thoroughly tested against the MRML (Rust) reference implementation to ensure compatibility and correctness.
Benchmark | Time | Memory | Allocs |
---|---|---|---|
BenchmarkMJMLRender_10_Sections-8 | 0.42ms | 0.56MB | 4.9K |
BenchmarkMJMLRender_10_Sections_Cache-8 | 0.22ms | 0.49MB | 2.8K |
BenchmarkMJMLRender_100_Sections-8 | 4.59ms | 5.83MB | 46.2K |
BenchmarkMJMLRender_100_Sections_Cache-8 | 2.73ms | 5.13MB | 26.9K |
BenchmarkMJMLRender_1000_Sections-8 | 42.68ms | 59.23MB | 459.4K |
BenchmarkMJMLRender_1000_Sections_Cache-8 | 23.66ms | 52.20MB | 267.2K |
BenchmarkMJMLRender_10_Sections_Memory-8 | 0.50ms | 0.56MB | 4.9K |
BenchmarkMJMLRender_10_Sections_Memory_Cache-8 | 0.28ms | 0.49MB | 2.8K |
BenchmarkMJMLRender_100_Sections_Memory-8 | 4.65ms | 5.83MB | 46.2K |
BenchmarkMJMLRender_100_Sections_Memory_Cache-8 | 2.65ms | 5.13MB | 26.9K |
BenchmarkMJMLRender_1000_Sections_Memory-8 | 41.93ms | 59.23MB | 459.4K |
BenchmarkMJMLRender_1000_Sections_Memory_Cache-8 | 23.34ms | 52.20MB | 267.2K |
BenchmarkMJMLRender_100_Sections_Writer-8 | 2.44ms | 4.69MB | 21.2K |
For comprehensive performance analysis including comparisons with other MJML implementations, see our dedicated performance benchmarks documentation.
# Run comparative benchmarks
./bench-austin.sh --markdown
# Run internal Go benchmarks
./bench.sh
The Go implementation follows a clean, modular architecture inspired by Go best practices:
go/
βββ cmd/gomjml/ # CLI application
β βββ main.go # Minimal entry point
β βββ command/ # Individual CLI commands
β βββ root.go # Root command setup
β βββ compile.go # MJML compilation command
β βββ test.go # Test runner command
β
βββ mjml/ # Core MJML library (importable)
β βββ component.go # Component factory and interfaces
β βββ render.go # Main rendering logic and MJMLComponent
β βββ mjml_test.go # Library unit tests
β βββ integration_test.go # MRML comparison tests
β β
β βββ components/ # Individual component implementations
β β βββ base.go # Shared Component interface and BaseComponent
β β βββ head.go # mj-head, mj-title, mj-font components
β β βββ body.go # mj-body component
β β βββ section.go # mj-section component
β β βββ column.go # mj-column component
β β βββ text.go # mj-text component
β β βββ button.go # mj-button component
β β βββ image.go # mj-image component
β β
β βββ testdata/ # Test MJML files
β βββ basic.mjml
β βββ with-head.mjml
β βββ complex-layout.mjml
β
βββ parser/ # MJML parsing package (importable)
βββ parser.go # XML parsing logic with MJMLNode AST
βββ parser_test.go # Parser unit tests
MJML Input β XML Parser β AST β Component Tree β HTML Output
β β β
Validation Attribute CSS Generation
Processing & Email Compatibility
- Package Separation: Clean separation between CLI, library, and parsing concerns
- Component System: Consistent Component interface with embedded BaseComponent
- Email Compatibility: MSO/Outlook conditional comments and email-safe CSS
- Responsive Design: Mobile-first CSS with media queries
- Testing Strategy: Direct library testing without subprocess dependencies
MRML is a Rust implementation of the MJML email framework that provides a fast, native alternative to the original JavaScript implementation. This Go implementation uses MRML as its reference for testing compatibility and correctness.
Why did I choose MRML as a reference, rather than the default MJML compiler?
- Performance: Native Rust performance comparable to our Go implementation
- Compatibility: Produces the same MJML-compliant HTML output as the JavaScript version
- Reliability: Well-tested, production-ready implementation
- Accessibility: Already installed and working in our development environment
The comprehensive test suite validates output against MRML:
# Run all tests via CLI
./bin/gomjml test
# Run with verbose output
./bin/gomjml test -v
# Run specific test pattern
./bin/gomjml test -pattern "basic"
# Direct Go testing
cd mjml && go test -v
- Fast Compilation: Native Go performance, typically sub-millisecond for basic templates
- Memory Efficient: Minimal allocations during parsing and rendering
- Scalable: Handles complex MJML documents with multiple sections and components
When to Enable Caching:
- High-volume applications rendering the same templates repeatedly
- Web servers with template reuse patterns
- Batch processing where templates are rendered multiple times
- Applications where parsing time > rendering time
Memory Management:
- Default TTL: 5 minutes per cached template
- Memory Usage: ~5-50KB per cached template (varies by complexity)
- Growth Pattern: Cache grows between cleanup cycles, shrinks during cleanup
- No Size Limits: Monitor memory usage in production environments
Thread Safety:
- All cache operations are safe for concurrent use
- Singleflight pattern prevents duplicate parsing under high load
- Background cleanup runs automatically every 2.5 minutes (default)
Configuration:
// Set cache TTL before first use (call only once)
mjml.SetASTCacheTTLOnce(10 * time.Minute)
// Set cleanup interval (call only once)
mjml.SetASTCacheCleanupIntervalOnce(5 * time.Minute)
// For graceful shutdown in long-running applications (optional)
// Not needed for CLI tools or short-lived processes
defer mjml.StopASTCacheCleanup()
When NOT to Use Caching:
- Single-use template rendering
- Memory-constrained environments
- Applications with constantly changing templates
- Short-lived processes where cache warmup overhead > benefits
Generated HTML works across all major email clients:
- Microsoft Outlook (2007, 2010, 2013, 2016, 365)
- Gmail (Web, iOS, Android)
- Apple Mail (macOS, iOS)
- Thunderbird
- Yahoo Mail
- Outlook.com / Hotmail
- MSO Conditional Comments: Outlook-specific styling and layout fixes
- CSS Inlining Ready: Structure compatible with CSS inlining tools
- Mobile Responsive: Automatic mobile breakpoints and media queries
- Web Font Support: Google Fonts integration with fallbacks
- MJML - Original JavaScript implementation and framework specification
- MRML - Rust implementation used as reference for testing
- MJML Documentation - Official MJML component specification
- MJML Try It Live - Online MJML editor and tester