mustachio-ruby: The Postmark template engine in Ruby
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.