A Simple Way to Add a Title and Other SEO Metadata to a Phoenix App

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.

What you'll need

  1. Erlang, Elixir and Phoenix Installed
  2. A Phoenix App

Step 1: The Config

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"
}

Step 2: The Meta Template

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

Step 3: The View Modules

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

Step 4: The Meta Module

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

Step 5: The MyAppWeb Module

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

Conclusion

Quick Tutorial Checklist

  1. Added Default Metadata Values to config/config.exs
  2. Created lib/my_app_web/templates/layout/meta.html.eex and added metadata fields
  3. Added the meta template to lib/my_app_web/templates/layout/layout.html.eex
  4. Added some metadata/2 functions to the view modules
  5. Created the lib/my_app_web/meta.ex module
  6. Added a line to use the Meta file in lib/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.

Conner Fritz

Hey, I'm Conner, and I'm a Software Developer turned Engineering Leader based in Toronto. I fell in love with coding as a child, teaching myself how to use the power of code to solve problems and bring my ideas to reality. It's been an important part of my life ever since.