Titles, metadata, tags. All crucial parts of a website. When using Phoenix for anything beyond an API, you need to be able to add this data to your website's pages. Due to the order that Phoenix performs rendering, the Rails way of adding this data through content_for
doesn't work, so we need to be a bit creative.
People use and have shared a bunch of different methods for achieving this in Phoenix, but for me, none of them felt quite right. I was looking for an easily configurable system that could support adding information from the database and could fall back on default values when no configuration exists for a page.
In this tutorial, I'll share the solution I've arrived at. It achieves the core goals I set out trying to achieve and feels pretty good to me. I hope you'll agree and find it useful.
We're going to keep this pretty short and sweet, so let's begin.
First, let's add some nice default configuration for the meta fields we're going to use. For this tutorial, we're going to add a title, description, and tags to our pages, so let's configure them with some default values.
config/config.exs
...
config :my_app, MyAppWeb.Meta, %{
title: "My App",
description: "My App is my app.",
tags: "phoenix,tutorial"
}
Now, let's make a meta.html.eex
template where we'll keep the HTML structure of our meta tags. For now, we're going to just use the title
, description
, and tags
tags that we defined in the configuration, but you can add anything you want/need here. The get_metadata/1
function will be responsible for returning the map of meta values.
lib/my_app_web/templates/layout/meta.html.eex
<title><%= get_metadata(@conn)[:title] %></title>
<meta name="description" content="<%= get_metadata(@conn)[:description] %>" />
<meta name="tags" content="<%= get_metadata(@conn)[:tags] %>" />
Then we render it in layout.html.eex
.
lib/my_app_web/templates/layout/layout.html.eex
<%= render "meta.html", assigns %>
# Above the script elements replacing the default <title></title> tag
Next, we're going to define our metadata for different pages and patterns. We'll start with the standard Phoenix homepage (:index
in the PageView
module).
lib/my_app_web/views/page_view.ex
defmodule MyAppWeb.PageView do
use MyAppWeb, :view
def metadata(:index, _), do: %{
title: "My App | Hello Homepage"
}
end
We only need to define the values we want to change, as the system will use the defaults we defined in the first step for anything we don't specify here.
An additional use case would involve setting the title to match a field on an item from the database. Let's say you had a %Post{}
with a title
field which you want to appear in the title of the page: you can get that value from the assigns
passed to the metadata/2
you define in the PostsView
module.
lib/my_app_web/views/posts_view.ex
defmodule MyAppWeb.PostsView do
use MyAppWeb, :view
def metadata(:index, _), do: %{
title: "Blog | My App"
}
# get the
def metadata(:show, %{post: post}}), do: %{
title: "#{post.title} | My App",
description: post.description,
tags: post.tags
}
end
Here is where we set up the core functionality for our metadata solution in a new Meta
module. We're going to use some of Elixir's metaprogramming functionality to make this all work. Meta-programming for meta-data, now that's meta!
First, we add a __before_compile__
macro that defines a version of the metadata
function which simply returns an empty map.
By defining this inside __before_compile__
this "default" method is added to all the view modules after our other definitions, so that the metadata falls back to the default values for any page that doesn't match a defined pattern. This saves us from having to define a method for every possible page and prevents errors if we happen to miss a page or pattern (or simply choose to leave a page using the defaults).
lib/my_app_web/meta.ex
defmodule MyAppWeb.Meta do
defmacro __before_compile__(_env) do
quote do
def metadata(_, _), do: %{}
end
end
end
Next, we define a __using__
block which will inject the values defined within into all of our view modules. Inside of the __using__
block we first make sure the module inserts what we defined in the __before_compile__
block by passing it to @before_compile
.
Then, we define the get_metadata/1
function we originally referenced in our meta.html.eex
file. This method uses pattern matching to extract the current action
, view
and assigns
from the conn
that it receives. It then calls the relevant metadata/2
function on the current view, passing it the action
and assigns
that are needed for the pattern-matching. Finally, it uses Map.merge/2
to merge the default configuration with the overrides it receives from metadata/2
.
Thanks to the __before_compile__
block, if no overrides are defined view.metadata/2
falls back to the one we defined above and the default configured values are returned.
lib/my_app_web/meta.ex (CONTINUED)
defmodule MyAppWeb.Meta do
defmacro __before_compile__(_env) do
quote do
def metadata(_, _), do: %{}
end
end
defmacro __using__(_) do
quote do
@before_compile unquote(__MODULE__)
def get_metadata(%{private: %{phoenix_action: action, phoenix_view: view}, assigns: assigns}) do
Map.merge(Application.get_env(:my_app, MyAppWeb.Meta), view.metadata(action, assigns))
end
end
end
end
Finally, set the view definition to use the Meta
module we just defined by adding it to the bottom of the view
definition.
lib/my_app_web.ex
def view do
quote do
# ...
use MyAppWeb.Meta
end
end
config/config.exs
lib/my_app_web/templates/layout/meta.html.eex
and added metadata fieldslib/my_app_web/templates/layout/layout.html.eex
metadata/2
functions to the view moduleslib/my_app_web/meta.ex
modulelib/my_app_web.ex
And that's all she wrote. You can customize this solution to add more types of metadata, such as OpenGraph fields and social images. SEO and social tags are pretty essential to a website succeeding, so I think having a dynamic and easy to apply approach here is crucial. I hope you enjoyed this tutorial and found it useful!
If you liked this you can also follow me on Twitter, check out my GitHub, and take a look at my other blog posts.