Phoenix email defaults: better templates using components
Switch out your Swoosh templates!
originally published
Swoosh has been a part of default Phoenix installs for some time. There is a package called phoenix_swoosh that adds some templating stuff on top of swoosh, that is somewhat popular as far as I know. Modern Phoenix, with the addition of heex templates and Phoenix.Component , kind of changes up the game though.
With heex templates, I personally don’t see much of a need for phoenix_swoosh anymore. Instead, I’m going to show you what I’ve been doing in fresh Phoenix projects to get:
- Nice enough looking transactional emails
- Generating html and text bodies from one piece of content
- Simple email layouts with components
Let’s check out an example from a project where I implemented a magic-link style login recently:
defmodule ExampleWeb.Users.UserNotifier do
import Swoosh.Email
# Add this:
import Phoenix.Component
alias ExampleWeb.Mailer
# And then add a component for your email content:
defp login_content(assigns) do
~H"""
<h1>Hey there!</h1>
<p>Please use this link to sign in to MyApp:</p>
<a href={@url}><%= @url %></a>
<p>If you didn't request this email, feel free to ignore this.</p>
"""
end
end
We’ll start with a heex component that has our html content. Then we’ll add a function to construct the swoosh email, and turn this template into html:
defmodule ExampleWeb.Users.UserNotifier do
import Swoosh.Email
import Phoenix.Component
alias ExampleWeb.Mailer
defp login_content(assigns) do
~H"""
<h1>Hey there!</h1>
<p>Please use this link to sign in to MyApp:</p>
<a href={@url}><%= @url %></a>
<p>If you didn't request this email, feel free to ignore this.</p>
"""
end
# Pretty traditional swoosh-setup stuff.
def deliver_magic_link(user, url) do
template = login_content(%{url: url})
html = heex_to_html(template)
email =
new()
|> to(user.email)
|> from({"MyApp", "[email protected]"})
|> subject("Sign in to MyApp")
|> html_body(html)
with {:ok, _metadata} <- Mailer.deliver(email) do
{:ok, email}
end
end
# And a function to render our heex template.
defp heex_to_html(template) do
template
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
end
So far so good. Pay attention to how we render the html: we need to provide the assigns map, which we construct when we call login_content . And we also use a helper function to render it down to a string. You could stop here if you want. But let’s also generate text bodies from the html. I’ll snip out the context now, but it goes in UserNotifier :
def deliver_magic_link(user, url) do
template = login_content(%{url: url})
html = heex_to_html(template)
text = html_to_text(html)
email =
new()
|> to(user.email)
|> from({"MyApp", "[email protected]"})
|> subject("Sign in to MyApp")
|> html_body(html)
|> text_body(text)
with {:ok, _metadata} <- Mailer.deliver(email) do
{:ok, email}
end
end
defp heex_to_html(template) do
template
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
defp html_to_text(html) do
html
|> Floki.parse_document!()
|> Floki.find("body")
|> Floki.text(sep: "\n\n")
end
Note: we’re selecting the body here when we parse the document because we don’t want to render anything in the head tag. Secondly, the sep: "\n\n" part tells Floki to insert two newlines between nodes when turning it into text. If you don’t do this, it becomes one big run-on paragraph.
Every new Phoenix app includes Floki, so you can just do this out of the box, however, you’re going to need to enable it in non-test environments. Remove the only: :test part from the Floki line in your mix.exs , so you have:
defp deps do
[
{:floki, "~> 0.35.0"}
]
end
Finally, lets use components to make a little layout system:
defp email_layout(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
font-family: system-ui, sans-serif;
margin: 3em auto;
overflow-wrap: break-word;
word-break: break-all;
max-width: 1024px;
padding: 0 1em;
}
</style>
</head>
<body>
<%= render_slot(@inner_block) %>
</body>
</html>
"""
end
def login_content(assigns) do
~H"""
<.email_layout>
<h1>Hey there!</h1>
<p>Please use this link to sign in to MyApp:</p>
<a href={@url}><%= @url %></a>
<p>If you didn't request this email, feel free to ignore this.</p>
</.email_layout>
"""
end
I provided some basic styles here I kind of like. Experiment with it! Of course, you probably already know that CSS in email is… a minefield to say the least. If you use non-inline CSS in your emails, prepare for it to possibly not work in some environments.
For purposes of transactional emails, I think this approach is ok. I’m fine if the styles don’t work in some browsers. I just want to be able to log people in, or reset their passwords, or other stuff like that. I’m not doing email marketing in my Phoenix apps. If you are, I encourage you to look into more comprehensive solutions like MJML .
Here’s the final comprehensive solution:
defmodule ExampleWeb.Users.UserNotifier do
import Swoosh.Email
import Phoenix.Component
alias ExampleWeb.Mailer
defp email_layout(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
font-family: system-ui, sans-serif;
margin: 3em auto;
overflow-wrap: break-word;
word-break: break-all;
max-width: 1024px;
padding: 0 1em;
}
</style>
</head>
<body>
<%= render_slot(@inner_block) %>
</body>
</html>
"""
end
def login_content(assigns) do
~H"""
<.email_layout>
<h1>Hey there!</h1>
<p>Please use this link to sign in to MyApp:</p>
<a href={@url}><%= @url %></a>
<p>If you didn't request this email, feel free to ignore this.</p>
</.email_layout>
"""
end
def deliver_magic_link(user, url) do
template = login_content(%{url: url})
html = heex_to_html(template)
text = html_to_text(html)
email =
new()
|> to(user.email)
|> from({"MyApp", "[email protected]"})
|> subject("Sign in to MyApp")
|> html_body(html)
|> text_body(text)
with {:ok, _metadata} <- Mailer.deliver(email) do
{:ok, email}
end
end
defp heex_to_html(template) do
template
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
defp html_to_text(html) do
html
|> Floki.parse_document!()
|> Floki.find("body")
|> Floki.text(sep: "\n\n")
end
end
And there it is! We have a pretty ok looking transactional email in our Phoenix app, with very little code required. Here’s what these styles more or less look like at a tablet-ish size: