https://camillovisini.com/coding/phlex-for-rails-emails-action-mailer-without-erb
Camillo Visini
CV
Coding Writing Reading Drawing About
Phlex for Rails Emails: Action Mailer without ERB
Rendering Action Mailer emails with Phlex components and layouts:
Clean, Composable, and Completely Ruby
Published on February 27, 2025 ·
Updated on March 02, 2025
Writing Ruby all day is fun, but writing Ruby in HTML is not. In the
past months, I found switching from Ruby to ERB views and partials to
be interrupting my flow and therefore increasingly annoying. Enter
Phlex, which is a Ruby library that allows you to write HTML
(components, views, layouts) in pure Ruby. After already completely
ditching ERB for Phlex in Rails views for multiple applications, I
wanted to see if I could do the same for Action Mailer. ERB be gone!
Why might this be interesting, besides just trying it out for the
sake of it, and to see if it's possible? I see these benefits:
* No more ERB: Use Phlex all the way, even for mailers
* Manage the HTML and text version of the email in one place
* Use Phlex components for layouts and views
As for my part, I could gladly live without ERB in my Rails
applications. After all, if it does not spark joy when you touch it,
you should get rid of it.
Check out the repository for the full code. I hope you find this as
exciting as I do!
Project source code available at...
visini/rails-phlex-action-mailer
Let's begin by setting up a mailer in our Rails app so we can
demonstrate how to move everything to Phlex.
The ERB Way
The UserMailer class is our entry point into Action Mailer, which
handles email delivery in Rails. Nothing special to see here, this is
Rails 101. It's basically what we get when we generate a mailer
(e.g., rails g mailer UserMailer).
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome
@user = params[:user]
mail(to: @user.email, subject: "Welcome")
end
end
We also need some corresponding view files or "email templates". Not
all email clients render HTML, so we need to provide both a HTML and
a text version. We can use ERB to use Ruby code in the templates (for
example, to access instance variables or to use helper methods). In
terms of files, this means that we need one template for the HTML
version:
app/views/user_mailer/welcome.html.erb
We are excited to have you join us! Thank you for signing up.
<%= link_to 'Click here to sign in', 'https://example.com' %>
And one for the text version:
app/views/user_mailer/welcome.text.erb
We are excited to have you join us! Thank you for signing up.
Click here to sign in: https://example.com
We also need to add some basic layouts. In the layout, you might add
a logo at the top of the email, place links in a footer, add contact
information, and so on.
Because these parts are in the layout, they become easy to manage:
When you want to change the layout, you can change everything in one
place. For now, we keep it simple, and just add the application name
and a "signature" at the end, to demonstrate how the layout wraps
around the email content.
app/views/layouts/mailer.html.erb
Application Name
<%= yield %>
Best Regards,
Company Name
For the text version, we mirror the of the HTML version. Since we
can't use HTML, we need to keep it simple and work with plain text.
We may use line breaks or adding horizontal rules to separate
sections, but for now we keep it simple:
app/views/layouts/mailer.text.erb
Application Name
<%= yield %>
Best Regards,
Company Name
The above template plus their layout produces the following email
messages. First, the HTML version:
Application Name
We are excited to have you join us! Thank you for signing up.
Click here to sign in
Best Regards,
Company Name
And the text version:
Application Name
We are excited to have you join us! Thank you for signing up.
Click here to sign in: https://example.com
Best Regards,
Company Name
As you can see, the email content is rendered with the layout wrapped
around it.
By creating a test, we can assert the email content for the HTML and
text version via fixtures. We can leverage the read_fixture helper
(see the Rails Guide on Testing) to load the fixture files and
compare them to the email content. We create a simple helper method
assert_mailer_fixtures to conveniently assert both versions.
test/mailers/user_mailer_test.rb
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "welcome" do
user = User.new(email: "[email protected]")
email = UserMailer.with(user: user).welcome
assert_equal ["[email protected]"], email.from
assert_equal ["[email protected]"], email.to
assert_equal "Welcome", email.subject
assert_mailer_fixtures("welcome", email)
end
private
# This checks the email content against the fixtures.
# For example, for UserMailer#welcome, it checks against:
# test/fixtures/user_mailer/welcome.txt
# test/fixtures/user_mailer/welcome.html
def assert_mailer_fixtures(name, email)
email_body_html = email.html_part.body.to_s
email_body_text = email.text_part.body.to_s.strip
expected_html = read_fixture("#{name}.html").join.strip
expected_text = read_fixture("#{name}.txt").join.strip
fixture_html = Nokogiri::HTML.parse(expected_html).to_html.strip
email_html = Nokogiri::HTML.parse(email_body_html).to_html.strip
assert_dom_equal fixture_html, email_html
assert_equal expected_text, email_body_text
end
end
We now have a basic setup for sending emails with Action Mailer - the
ERB way. The next step is to refactor the views and layouts to use
Phlex. This test ensures that while we refactor, we produce exactly
the same email content (HTML and text) while we move from ERB to
Phlex.
Using Phlex Views for Email Templates
To install Phlex in our Rails app, we follow the installation
instructions. In particular, we run the install generator to create
base classes and directories for our views (the generator provides an
opinionated starting point with a folder structure and naming
convention for views and components).
Then, we can create the Phlex views for the email templates. We might
place all mailer views under app/views/mailers and create a subfolder
for each mailer. Similar to with the ERB templates, we need to create
a view for the HTML version and one for the text version. First, we
create the HTML view:
app/views/mailers/users/welcome/html.rb
class Views::Mailers::Users::Welcome::Html < Views::Base
def initialize(user)
@user = user
end
def view_template
div do
plain "We are excited to have you join us! Thank you for signing up."
end
a href: "https://example.com" do
plain "Click here to sign in"
end
end
end
And then the text view:
app/views/mailers/users/welcome/text.rb
class Views::Mailers::Users::Welcome::Text < Views::Base
def initialize(user)
@user = user
end
def view_template
plain "We are excited to have you join us! Thank you for signing up."
plain "\n\n"
plain "Click here to sign in: https://example.com"
end
end
Inside the ERB templates, we render the Phlex views. For the HTML
version:
app/views/user_mailer/welcome.html.erb
<%= render Views::Mailers::Users::Welcome::Html.new(user: @user) %>
And for the text version:
app/views/user_mailer/welcome.text.erb
<%= render Views::Mailers::Users::Welcome::Text.new(user: @user) %>
Now what? This seems like a tiny step forward. We have the same
content, only now it's rendered via Phlex views instead of ERB
templates, but we still need the ERB files as an "entry point". Even
worse, we have gone from two files to four files for the templates,
which adds overhead - not ideal! But we are not done yet. Let's have
a look at layouts next, Phlex style.
Setting up Phlex Layouts for Mailers
Similar to the ERB layouts, we can create Phlex layouts for our email
templates. But first, let's tell Action Mailer to not use any
layouts, because we will compose the templates on our own from
layouts and views with Phlex.
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout false
end
We can delete app/views/layouts/mailer.html.erb and app/views/layouts
/mailer.text.erb as they are no longer needed. Instead, we create
Phlex layouts for the email templates - you could use a tool like
phlexing.fun to convert HTML to Phlex. First, we create a layout for
the HTML version:
app/components/layouts/mailers/html.rb
class Components::Layouts::Mailers::Html < Phlex::HTML
def view_template(&block)
doctype
html do
head do
meta charset: "utf-8"
end
body do
div do
b { "Application Name" }
end
yield block
div { "Best Regards," }
div { "Company Name" }
end
end
end
end
And one for the text version:
app/components/layouts/mailers/text.rb
class Components::Layouts::Mailers::Text < Phlex::HTML
def view_template(&block)
plain "Application Name"
plain "\n\n"
yield block
plain "\n\n\n"
plain "Best Regards,"
plain "\n"
plain "Company Name"
end
end
In the views, we use the around_template hook to tell the view to
render with the particular layout (see Phlex layout docs).
For the HTML variant, this means we need something like this:
app/views/mailers/users/welcome/html.rb
class Views::Mailers::Users::Welcome::Html < Views::Base
include Components
def initialize(user)
@user = user
end
def around_template
render Components::Layouts::Mailers::Html.new do
super
end
end
def view_template
div do
plain "We are excited to have you join us! Thank you for signing up."
end
a href: "https://example.com" do
plain "Click here to sign in"
end
end
end
And for the text variant:
app/views/mailers/users/welcome/text.rb
class Views::Mailers::Users::Welcome::Text < Views::Base
include Components
def initialize(user)
@user = user
end
def around_template
render Components::Layouts::Mailers::Text.new do
super
end
end
def view_template
plain "We are excited to have you join us! Thank you for signing up."
plain "\n\n"
plain "Click here to sign in: https://example.com"
end
end
Now we have the email content rendered with the layout wrapped around
it. But, for each email template, we need to repeat a lot of the same
code (e.g., explicitly defining around_template hook for every single
templates, duplicated initialize for HTML and text variant). We're
not there yet, though. In order to DRY up the code, we need to
refactor the views and how we're rendering the emails.
Refactoring ApplicationMailer and Views
Here comes the cool part. We can refactor the ApplicationMailer class
to render the email content with the layout by way of a custom method
render_email (we use layouts through composition). This way, we can
remove the around_template hook which is cluttering up the views and
thereby make them more compact. Also, we go from two separate views
to one view class with two subclasses for the HTML and text version.
No need to always remember to also change the text version when
updating the HTML version - it's all in one place and almost
impossible to miss. Nice!
Let's first have a look at the refactored ApplicationMailer class:
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout false
def render_email(view_class, subject:, to:, view_params: {})
mail({subject:, to:, content_type: "multipart/alternative"}) do |format|
format.html {
render Components::Layouts::Mailers::Html.new {
render view_class.const_get(:Html).new(**view_params)
}
}
format.text {
render Components::Layouts::Mailers::Text.new {
render view_class.const_get(:Text).new(**view_params)
}
}
end
end
end
The render_email method takes the view class and additional
arguments, forwarding them to the Action Mailer mail method. In the
block, it then renders the email content with the layout for the HTML
and text versions.
Here's how we render the email from the mailer class:
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome
@user = params[:user]
render_email(
Views::Mailers::Users::Welcome,
subject: "Welcome",
to: @user.email,
view_params: {user: @user}
)
end
end
In the views, we now centralize the HTML and text content in one
class. It's easy to manage and change the content in one place, no
need to jump between different files.
app/views/user_mailer/welcome.rb
class Views::Mailers::Users::Welcome < Views::Base
def initialize(user:)
@user = user
end
class Html < self
def view_template
div do
plain "We are excited to have you join us! Thank you for signing up."
end
a href: "https://example.com" do
plain "Click here to sign in"
end
end
end
class Text < self
def view_template
plain "We are excited to have you join us! Thank you for signing up."
plain "\n\n"
plain "Click here to sign in: https://example.com"
end
end
end
We can now delete these four files:
* app/views/mailers/users/welcome/html.rb
* app/views/mailers/users/welcome/text.rb
* app/views/user_mailer/welcome.html.erb
* app/views/user_mailer/welcome.text.erb
By deleting the email templates (and before that, the layouts), we
have removed all ERB and now can write our emails in plain Ruby with
Phlex.
Inlining Styles
Let's add some finishing touches - after all, we want our emails to
look good. But, email clients have limited support for CSS
stylesheets, so we need to use inline styles. We can add a style tag
to our HTML layout to get started. Something like this:
app/components/layouts/mailers/html.rb
class Components::Layouts::Mailers::Html < Phlex::HTML
def view_template(&block)
doctype
html do
head do
meta charset: "utf-8"
style do
plain <<~CSS
body {
font-family: Arial, sans-serif;
}
.highlight {
background-color: yellow;
}
.signature {
font-style: italic;
}
CSS
end
end
body do
div do
b { "Application Name" }
end
yield block
div { "Best Regards," }
div(class: "signature") { "Company Name" }
end
end
end
end
However, for emails, it's generally recommended to inline the styles
of all classes we use by adding the styles to the respective
elements. Let's add the roadie-rails gem to the mix, so that classes
like highlight and signature are inlined with their actual style
values (this works for both the layout, as shown above with
signature, as well as within the views, e.g., when you use highlight
for a particular element).
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
include Roadie::Rails::Automatic
default from: "[email protected]"
layout false
#...
end
With roadie-rails in place, we can now use classes in our views and
layouts, and the styles will be inlined automatically. Note that
roadie-rails inlines the styles only when delivering - use
UserMailer.with(user: user).welcome.deliver in Rails console to check
the inlined styles. Here's an example of the resulting HTML email
content:
Application Name
We are excited to have you join us! Thank you for signing up.
Click here to sign in
Best Regards,
Company Name
Conclusion
By moving from ERB to Phlex for Action Mailer, we can write our email
templates in pure Ruby. We can manage the HTML and text version in
one place, and use Phlex components for layouts and views. Not a
single ERB file left in our entire Rails app! Check out the
repository for the full code:
Project source code available at...
visini/rails-phlex-action-mailer
I hope this is helpful for those looking into using Phlex for mailers
as the last piece of the puzzle to get rid of ERB completely. If you
have any questions or feedback, feel free to reach out.
Keep Phlexing!
Let's stay in touch
LinkedIn GitHub X/Twitter BlueSky Mastodon
Sign up for my personal newsletter
Enter your email address below in order to receive an email when I
write a new article:
[ ]
Subscribe
Note: You can unsubscribe at any time by clicking on the unsubscribe
link included in every email.
Picture of Camillo Visini
Camillo Visini
CEO & Founder · Visini AG
Published on February 27, 2025
Share Article
(c) 2025 Camillo Visini
Dark Light Imprint RSS