Email development in Rails often involves tedious cycles: change code, trigger the mailer, check the inbox. Action Mailer previews eliminate this friction by rendering emails directly in the browser. While most developers know the basics, Rails 8 offers powerful preview features that remain underutilized.
The Problem with Email Development
Building transactional emails requires testing multiple scenarios: welcome emails with different user names, order confirmations with varying line items, password resets with expiring tokens. Without previews, developers must seed data, trigger actions, and wait for email delivery—a workflow that kills productivity.
Action Mailer previews solve this by rendering emails at a URL, but the default setup barely scratches the surface. Dynamic parameters, realistic attachments, and multi-part email testing require deliberate configuration.
Setting Up Dynamic Previews
Basic previews use hardcoded data, making them brittle and limited. A better approach accepts URL parameters to render different scenarios on demand.
# test/mailers/previews/order_mailer_preview.rb
class OrderMailerPreview < ActionMailer::Preview
def confirmation
order = find_or_build_order
OrderMailer.confirmation(order)
end
private
def find_or_build_order
if params[:order_id]
Order.find(params[:order_id])
else
build_sample_order
end
end
def build_sample_order
Order.new(
id: 12345,
user: User.new(name: params[:name] || "Jane Developer", email: "[email protected]"),
total_cents: (params[:total] || 9999).to_i,
line_items: build_line_items,
created_at: Time.current
)
end
def build_line_items
count = (params[:items] || 3).to_i
count.times.map do |i|
LineItem.new(
name: "Product #{i + 1}",
quantity: rand(1..3),
price_cents: rand(1000..5000)
)
end
end
endThis preview responds to URL parameters: /rails/mailers/order_mailer/confirmation?name=Alex&total=15000&items=5 renders an order for Alex with $150 total and five line items. The params method in previews works identically to controller params, enabling flexible scenario testing without database dependencies.
Testing Multi-Part Emails
Professional emails require both HTML and plain text versions. Rails generates these from separate templates, but previews default to showing only HTML. Testing both formats requires explicit configuration in the mailer and preview awareness.
# app/mailers/notification_mailer.rb
class NotificationMailer < ApplicationMailer
def weekly_digest(user)
@user = user
@articles = Article.published.where("created_at > ?", 1.week.ago).limit(10)
@unsubscribe_token = user.signed_id(purpose: :unsubscribe, expires_in: 30.days)
mail(
to: @user.email,
subject: "Your Weekly Digest - #{Date.current.strftime('%B %d')}"
) do |format|
format.html
format.text
end
end
end# app/views/notification_mailer/weekly_digest.html.erb
.article { margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; }
.article h3 { margin: 0 0 10px; color: #333; }
.unsubscribe { color: #999; font-size: 12px; margin-top: 30px; }
Hello <%= @user.name %>,
Here's what you missed this week:
<% @articles.each do |article| %>
<% end %>
# app/views/notification_mailer/weekly_digest.text.erb
Hello <%= @user.name %>,
Here's what you missed this week:
<% @articles.each do |article| %>
<%= article.title %>
<%= truncate(article.excerpt, length: 150) %>
Read more: <%= article_url(article) %>
<% end %>
---
Unsubscribe: <%= unsubscribe_url(token: @unsubscribe_token) %>When viewing the preview at /rails/mailers/notification_mailer/weekly_digest, Rails displays format toggles at the top of the preview window. Click between HTML, Text, and Raw views to verify both templates render correctly.
Preview Attachments Realistically
Emails with attachments—invoices, reports, receipts—need preview support. Generating realistic attachments in previews requires either fixture files or dynamic generation.
# test/mailers/previews/invoice_mailer_preview.rb
class InvoiceMailerPreview < ActionMailer::Preview
def send_invoice
invoice = build_sample_invoice
InvoiceMailer.send_invoice(invoice, attach_pdf: params[:pdf] != "false")
end
private
def build_sample_invoice
Invoice.new(
id: 1001,
user: User.new(name: "Acme Corp", email: "[email protected]"),
issued_at: Date.current,
due_at: 30.days.from_now,
line_items: [
OpenStruct.new(description: "Consulting Services", amount_cents: 500000),
OpenStruct.new(description: "Development Hours (40)", amount_cents: 600000)
]
)
end
end# app/mailers/invoice_mailer.rb
class InvoiceMailer < ApplicationMailer
def send_invoice(invoice, attach_pdf: true)
@invoice = invoice
if attach_pdf
pdf_content = generate_invoice_pdf(invoice)
attachments["invoice-#{invoice.id}.pdf"] = {
mime_type: "application/pdf",
content: pdf_content
}
end
mail(
to: invoice.user.email,
subject: "Invoice ##{invoice.id} from YourApp"
)
end
private
def generate_invoice_pdf(invoice)
# Using Prawn or similar PDF library
InvoicePdfGenerator.new(invoice).render
end
endVisit /rails/mailers/invoice_mailer/send_invoice to see the email with attachment indicator. The preview displays attachment metadata including filename and size. Toggle attachments off with ?pdf=false to test the email body without regenerating PDFs during development.
Organizing Previews by Scenario
Complex applications have dozens of email scenarios. Organize previews with descriptive method names and comments that appear in the preview index.
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
# Standard welcome email for new signups
def welcome_new_user
UserMailer.welcome(build_user(confirmed: false))
end
# Welcome email for OAuth users (no password set)
def welcome_oauth_user
UserMailer.welcome(build_user(provider: "google"))
end
# Password reset with 2-hour expiry warning
def password_reset_urgent
user = build_user
token = user.signed_id(purpose: :password_reset, expires_in: 2.hours)
UserMailer.password_reset(user, token)
end
# Account locked after failed attempts
def account_locked
user = build_user(locked_at: Time.current, failed_attempts: 5)
UserMailer.account_locked(user)
end
private
def build_user(attrs = {})
User.new({
id: 1,
name: params[:name] || "Test User",
email: "[email protected]",
created_at: Time.current
}.merge(attrs))
end
endThe preview index at /rails/mailers displays these methods with their comments as descriptions, creating a browsable catalog of email scenarios for designers and stakeholders.
Common Mistakes to Avoid
Several patterns cause preview headaches:
- Database dependencies: Previews should work without seeded data. Use
newinstead ofcreate, or guard finds with fallbacks. - Missing URL hosts: Emails need absolute URLs. Configure
default_url_optionsin the development environment or mailer previews fail on link helpers. - Ignoring text templates: Many email clients prefer plain text. Always create and preview both formats.
- Static timestamps: Use
Time.currentandDate.currentso previews reflect realistic dates.
Configuration for Development
Enable previews and configure them properly in development:
# config/environments/development.rb
Rails.application.configure do
config.action_mailer.show_previews = true
config.action_mailer.preview_paths << Rails.root.join("lib/mailer_previews")
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
endThe preview_paths configuration allows organizing previews outside the test directory—useful when previews contain substantial logic worth keeping closer to application code.
Summary
Action Mailer previews transform email development from a tedious chore into a rapid feedback loop. Dynamic parameters enable scenario testing without database manipulation. Multi-part templates ensure both HTML and text versions render correctly. Attachment support verifies that generated PDFs and files work as expected. Organized preview classes create a browsable catalog useful beyond development—product managers and designers can review emails without triggering real workflows.
For applications with complex transactional emails, investing in comprehensive previews pays dividends throughout the development lifecycle.