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