Skip to content
/ gomjml Public
forked from preslavrachev/gomjml

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

License

Notifications You must be signed in to change notification settings

epelc/gomjml

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

34 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

gomjml - Native Go MJML Compiler

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.

status Tests Go Report Card

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.

πŸš€ Features

  • 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

πŸ“¦ Installation

Install CLI

# 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

Install as Go Package

# Import as library
go get github.com/preslavrachev/gomjml

πŸ”§ Usage

Command Line Interface

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

CLI Commands

  • compile [input] - Compile MJML to HTML (main command)
  • test - Run test suite against MRML reference implementation
  • help - Show help information

Compile Command Options

  • -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)

Go Package API

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)
}

Adding New Components

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

Component Interface Requirements

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 performance
  • GetTagName() string: Returns the component's MJML tag name

For string-based rendering, use the helper function mjml.RenderComponentString(component) instead of a component method.

Delaying Component Implementation

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 Implementation Status

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

Implementation Summary

  • βœ… 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

Integration Test Status

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.

Performance Benchmarks

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

πŸ—οΈ Architecture

The Go implementation follows a clean, modular architecture inspired by Go best practices:

Project Structure

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

Processing Pipeline

MJML Input β†’ XML Parser β†’ AST β†’ Component Tree β†’ HTML Output
                ↓              ↓           ↓
            Validation    Attribute     CSS Generation
                         Processing    & Email Compatibility

Key Design Principles

  1. Package Separation: Clean separation between CLI, library, and parsing concerns
  2. Component System: Consistent Component interface with embedded BaseComponent
  3. Email Compatibility: MSO/Outlook conditional comments and email-safe CSS
  4. Responsive Design: Mobile-first CSS with media queries
  5. Testing Strategy: Direct library testing without subprocess dependencies

πŸ§ͺ Testing & MRML Compatibility

What is MRML?

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

Test Suite

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

πŸ“Š Performance & Compatibility

Performance Characteristics

  • 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

AST Caching (Opt-in Performance Feature)

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

Email Client Compatibility

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

Email-Specific Features

  • 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

πŸ”— Related Projects

  • 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

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Go 97.4%
  • Shell 2.6%