Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 54 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,16 @@ func main() {
</mjml>`

// Method 1: Direct rendering (recommended)
html, err := mjml.Render(mjmlContent)
html, err := mjml.RenderHTML(mjmlContent)
if err != nil {
log.Fatal("Render error:", err)
log.Fatal("RenderHTML error:", err)
}
fmt.Println(html)

// Method 1b: Direct rendering with debug attributes
htmlWithDebug, err := mjml.Render(mjmlContent, mjml.WithDebugTags(true))
htmlWithDebug, err := mjml.RenderHTML(mjmlContent, mjml.WithDebugTags(true))
if err != nil {
log.Fatal("Render error:", err)
log.Fatal("RenderHTML error:", err)
}
fmt.Println(htmlWithDebug) // Includes data-mj-debug-* attributes

Expand All @@ -133,7 +133,7 @@ func main() {

html, err = mjml.RenderComponentString(component)
if err != nil {
log.Fatal("Render error:", err)
log.Fatal("RenderHTML error:", err)
}
fmt.Println(html)
}
Expand Down Expand Up @@ -168,17 +168,22 @@ func NewMJNewComponent(node *parser.MJMLNode, opts *options.RenderOpts) *MJNewCo
// 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
func (c *MJNewComponent) RenderHTML(w io.StringWriter) error {
// Implementation here - write HTML directly to StringWriter
// Use c.AddDebugAttribute(tag, "new") for debug traceability

// Example implementation:
// if _, err := w.Write([]byte("<div>Hello World</div>")); err != nil {
// if _, err := w.WriteString("<div>Hello World</div>"); err != nil {
// return err
// }
return nil
}

func (c *MJNewComponent) RenderMJML(w io.StringWriter) error {
// Implementation for MJML-to-MJML rendering (for transformations)
return &NotImplementedError{ComponentName: "mj-new"}
}

func (c *MJNewComponent) GetTagName() string {
return "mj-new"
}
Expand All @@ -195,21 +200,26 @@ case "mj-new":

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
- **`RenderHTML(w io.StringWriter) error`**: Primary rendering method that writes HTML directly to a StringWriter for optimal performance
- **`RenderMJML(w io.StringWriter) error`**: MJML-to-MJML rendering method for transformations (can return NotImplementedError for most components)
- **`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.
For string-based rendering, use the helper functions `mjml.RenderComponentString(component)` for HTML output or `mjml.RenderComponentMJMLString(component)` for MJML output.

#### Delaying Component Implementation

If you need to register a component but won't implement its functionality right away, use the `NotImplementedError` pattern:

```go
func (c *MJNewComponent) Render(w io.Writer) error {
func (c *MJNewComponent) RenderHTML(w io.StringWriter) error {
// TODO: Implement mj-new component functionality
return &NotImplementedError{ComponentName: "mj-new"}
}

func (c *MJNewComponent) RenderMJML(w io.StringWriter) error {
return &NotImplementedError{ComponentName: "mj-new"}
}

func (c *MJNewComponent) GetTagName() string {
return "mj-new"
}
Expand Down Expand Up @@ -398,6 +408,39 @@ Generated HTML works across all major email clients:
- **Mobile Responsive**: Automatic mobile breakpoints and media queries
- **Web Font Support**: Google Fonts integration with fallbacks

## 🚀 Future Possibilities: Dual Rendering Architecture

The implementation now features a **dual rendering system** with both `RenderHTML()` and `RenderMJML()` methods, opening exciting possibilities for email template workflows:

### Code-First Email Templates
- **Generate Go Code from MJML**: Future tooling could parse MJML files and generate corresponding Go code that constructs component trees programmatically
- **Go as Single Source of Truth**: Keep Go code as the primary email template definition, with the ability to render both HTML (for sending) and MJML (for validation/debugging)
- **Round-trip Compatibility**: Generate MJML from Go component trees for verification against original MJML specifications or for use with other MJML tools

### Potential Workflow
```go
// Future: Generate this Go code from MJML input
func CreateNewsletterTemplate(title, content, ctaUrl string) mjml.Component {
return mjml.NewMJML().
AddHead(mjml.NewMJHead().
AddTitle(title)).
AddBody(mjml.NewMJBody().
AddSection(mjml.NewMJSection().
AddColumn(mjml.NewMJColumn().
AddText(content).
AddButton(mjml.NewMJButton().
SetHref(ctaUrl).
SetText("Click Me")))))
}

// Use the template
template := CreateNewsletterTemplate("News", "Hello World!", "https://example.com")
html, _ := mjml.RenderComponentString(template) // For email sending
mjmlCode, _ := mjml.RenderComponentMJMLString(template) // For validation/debugging
```

This approach enables **template-as-code** patterns where email templates can be version-controlled, tested, and maintained using standard software development practices while retaining full MJML compatibility.

## 🔗 Related Projects

- **[MJML](https://mjml.io/)** - Original JavaScript implementation and framework specification
Expand Down
6 changes: 3 additions & 3 deletions cmd/gomjml/command/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ Examples:
os.Exit(1)
}

// Render MJML to HTML using library
// RenderHTML MJML to HTML using library
var html string
if debug {
html, err = mjml.Render(string(mjmlContent), mjml.WithDebugTags(true))
html, err = mjml.RenderHTML(string(mjmlContent), mjml.WithDebugTags(true))
} else {
html, err = mjml.Render(string(mjmlContent))
html, err = mjml.RenderHTML(string(mjmlContent))
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error rendering MJML: %v\n", err)
Expand Down
6 changes: 3 additions & 3 deletions examples/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func main() {
</mjml>`

// Method 1: Direct rendering (recommended)
html, err := mjml.Render(mjmlContent)
html, err := mjml.RenderHTML(mjmlContent)
if err != nil {
log.Fatal("Render error:", err)
log.Fatal("RenderHTML error:", err)
}
fmt.Println(html)

Expand All @@ -43,7 +43,7 @@ func main() {

html, err = mjml.RenderComponentString(component)
if err != nil {
log.Fatal("Render error:", err)
log.Fatal("RenderHTML error:", err)
}
fmt.Println(html)
}
4 changes: 2 additions & 2 deletions examples/sendmail/sendmail.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func main() {
log.Fatalf("Failed to read MJML file: %v", err)
}

// Render MJML to HTML
html, err := mjml.Render(string(mjmlContent))
// RenderHTML MJML to HTML
html, err := mjml.RenderHTML(string(mjmlContent))
if err != nil {
log.Fatalf("MJML render error: %v", err)
}
Expand Down
36 changes: 18 additions & 18 deletions mjml/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ func BenchmarkMJMLRender_10_Sections(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand All @@ -89,9 +89,9 @@ func BenchmarkMJMLRender_100_Sections(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand All @@ -102,9 +102,9 @@ func BenchmarkMJMLRender_1000_Sections(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand All @@ -116,9 +116,9 @@ func BenchmarkMJMLRender_10_Sections_Memory(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand All @@ -130,9 +130,9 @@ func BenchmarkMJMLRender_100_Sections_Memory(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand All @@ -144,9 +144,9 @@ func BenchmarkMJMLRender_1000_Sections_Memory(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
}
Expand Down Expand Up @@ -207,9 +207,9 @@ func BenchmarkMJMLRender_100_Sections_Writer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buf strings.Builder
err := component.Render(&buf)
err := component.RenderHTML(&buf)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
_ = buf.String() // Force evaluation to match string-based benchmark
}
Expand All @@ -222,9 +222,9 @@ func BenchmarkMJMLRender_vs_RenderString_100_Sections(b *testing.B) {
b.Run("String-based", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := Render(template)
_, err := RenderHTML(template)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
})
Expand All @@ -242,9 +242,9 @@ func BenchmarkMJMLRender_vs_RenderString_100_Sections(b *testing.B) {
}

var buf strings.Builder
err = component.Render(&buf)
err = component.RenderHTML(&buf)
if err != nil {
b.Fatalf("Render failed: %v", err)
b.Fatalf("RenderHTML failed: %v", err)
}
}
})
Expand Down
Loading
Loading