devjoaov

mustachio-ruby: The Postmark template engine in Ruby

rubyrails

Use your Postmark templates in Ruby with this template engine port

Mustachio is the template engine that powers Postmark templates. While similar to Mustache, it has key differences in syntax for handling arrays and destructuring that make it uniquely suited for email templating.

You might wonder: why create another template engine when we already have excellent solutions like ERB, HAML, or Slim? I wouldn't recommend abandoning these tools for general web development. However, if you're already using Postmark templates, mustachio-ruby can help reduce your dependency on Postmark's service.

At Bleu, most of our projects use Postmark for transactional emails. We love their service and build our templates directly in their platform. However, in 2024, Postmark experienced an incident where emails took significantly longer to deliver. OTP codes were delayed by 40+ minutes. While rare, this incident made us realize we couldn't be completely dependent on a single email service. We needed a fallback system using an alternative SMTP provider like Resend.

The Problem: Template Lock-in

The quick solution was to create a fallback system that used Resend to send emails:

module FallbackMailing
  extend ActiveSupport::Concern

  included do
    if Rails.application.config.use_fallback_mailer_service
      def template_model=(model)
        @template_model = model
      end

      def template_model
        @template_model
      end
    end
  end
end

class ApplicationMailer < ActionMailer::Base
  default from: "no-reply@example.com"

  if Rails.application.config.use_fallback_mailer_service
    include FallbackMailing
  else
    include PostmarkRails::TemplatedMailerMixin

    def mail(headers = {}, &block)
      # remove not allowed attributes when using Postmark
      %i[subject header body content_type].each { |key| headers.delete(key) }
      super
    end
  end
end

This solution allowed us to send emails through alternative SMTP providers, but we faced another challenge: our templates were stored exclusively in Postmark.

We didn't want to abandon Postmark or maintain duplicate template versions. Our solution was to sync templates—keeping identical copies of our Postmark templates in our codebase. However, we discovered that Postmark's template engine was written in C#, not Ruby. This led us to create mustachio-ruby: a Ruby port of the original Mustachio template engine.

Installation

Add this line to your application's Gemfile:

gem 'mustachio-ruby'

And then execute:

bundle install

How mustachio-ruby Works

Core Components

The template engine consists of four main components that work together to parse, compile, and render templates:

1. Tokenizer (lib/mustachio_ruby/tokenizer.rb)

Parses raw template strings into structured tokens using regex patterns to identify template elements like variables, conditionals, and loops.

2. Parser (lib/mustachio_ruby/parser.rb)

Transforms tokens into executable template functions that can be called with data.

3. Context Object (lib/mustachio_ruby/context_object.rb)

Manages data access during rendering, including:

  • Dot-notation access (user.profile.name)
  • Parent scope access (../)
  • Existence checks (exists? method)

4. Token Types (lib/mustachio_ruby/token_tuple.rb)

Defines template element types: HTML-escaped variables, raw variables, conditional blocks, array iteration, and negated conditionals.

Basic Usage

Simple Variable Interpolation

template = MustachioRuby.parse("Hello {{name}}!")
content = template.call({"name" => "World"})
# => "Hello World!"

Array Iteration

template = "{{#each Company.ceo.products}}<li>{{ name }} and {{version}} and has a CEO: {{../../last_name}}</li>{{/each}}"
renderer = MustachioRuby.parse(template)

model = {
  "Company" => {
    "ceo" => {
      "last_name" => "Smith",
      "products" => [
        { "name" => "name 0", "version" => "version 0" },
        { "name" => "name 1", "version" => "version 1" },
      ]
    }
  }
}

result = renderer.call(model)
# => 
# <li>name 0 and version 0 and has a CEO: Smith</li>
# <li>name 1 and version 1 and has a CEO: Smith</li>

Conditional Rendering

# Show content when user exists
template = MustachioRuby.parse("{{#user}}Welcome {{name}}!{{/user}}")

# Show content when user doesn't exist
template = MustachioRuby.parse("{{^user}}Please log in{{/user}}")

Configuration Options

options = MustachioRuby::ParsingOptions.new
options.disable_content_safety = true # Disable HTML escaping
options.source_name = "my_template" # For error reporting
options.token_expanders = [expander] # Custom extensions

template = MustachioRuby.parse(source, options)

HTML Email Templates

You can follow the Postmark template syntax to build your email templates and then use mustachio-ruby (or the original C# version) to interpolate your variables:

mustachio_template = <<~HTML
  <div>
    <h1>Hello {{name}}!</h1>
    {{#user}}
      <p>Welcome back!</p>
    {{/user}}
    {{^user}} <!-- negative check -->
      <p>Please log in</p>
    {{/user}}
    <ul>
      {{#each items}}
        <li>{{name}}: ${{price}}</li>
      {{/each}}
    </ul>
  </div>
HTML

data = {
  "name" => "John",
  "user" => { "name" => "John" },
  "items" => [
    { "name" => "Apple", "price" => 1.50 },
    { "name" => "Banana", "price" => 0.75 }
  ]
}

template = MustachioRuby.parse(mustachio_template)
result = template.call(data)

# =>
# <div>
#   <h1>Hello John!</h1>
#   <p>Welcome back!</p>
#   <ul>
#     <li>Apple: $1.5</li>
#     <li>Banana: $0.75</li>
#   </ul>
# </div>

Ruby on Rails Integration

To use mustachio-ruby in Rails, set the body to be the parsed HTML content and the mail content_type to be "text/html" (or "text/plain" if you're using mustachio to interpolate variables in a plain text string):

def mail(headers = {}, &)
  template_content = get_template(template_alias)
  options = MustachioRuby::ParsingOptions.new
  options.disable_content_safety = true
  renderer = MustachioRuby.parse(template_content, options)
  html_content = renderer.call(template_model.deep_stringify_keys)
  headers[:body] = html_content
  headers[:content_type] = "text/html"

  super
end

Final Thoughts

Postmark is a fantastic service, and we continue to use it every day. mustachio-ruby isn't a replacement—it's a safety net. It lets you render templates in pure Ruby and send emails even when you don't have access to Postmark.

If your team relies on Postmark templates, I hope mustachio-ruby gives you the flexibility to build resilient email systems without vendor lock-in.

Learn More