Subscribe now

Page-Specific Titles [06.13.2017]

Firestorm is coming along nicely. However, presently each page has the same title. This is a pretty awful user experience, and it also makes our pages perform poorly in search engines. Let's quickly add a facility to specify titles for each page in our app.

Project

In the resources section, I've linked to a couple of blog posts that cover different ways to handle this issue. The posts discuss various considerations that are great to keep in mind. One of the more compelling points is that the page's title is really a view-level concern. Consequently, we'll begin by defining a function on the LayoutView that handles our page titles, and using it in the app layout:

vim lib/firestorm_web/web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
    <!-- We'll pass in the conn so it can be used when
         generating the title string -->
    <title><%= page_title(@conn) %></title>
    <!-- ... -->
  </head>
  <!-- ... -->
</html>

Now we'll add the function to the LayoutView, leaving it static at present:

defmodule FirestormWeb.Web.LayoutView do
  # ...
  def page_title(conn), do: "Firestorm"
  # ...
end

That works. Next, we'd like to specify a different title for various pages. We'll begin with the Category controller's index action:

  def page_title(conn) do
    # Phoenix has a `view_module` function that can determine the view module
    # that is used for a given connection.
    #
    # We could define a case statement to segregate by view:
    case Phoenix.Controller.view_module(conn) do
      FirestormWeb.Web.CategoryView ->
        "Categories - Firestorm"
      _ ->
        "Firestorm"
    end
  end

This works, but of course this case statement will become a bit overbearing. We could break this into functions in each view module, but as one of the posts argues, these views are otherwise concerned with rendering the content of the page, while the layout is concerned with rendering the wrapper, including the <head> element. With that in mind, let's extract a function in the LayoutView that takes the view module, the action name, and our connection's assigns, and returns our preferred title.

  def page_title(conn) do
    view = Phoenix.Controller.view_module(conn)
    action = Phoenix.Controller.action_name(conn)
    get_page_title(view, action, conn.assigns)
  end

  defp get_page_title(FirestormWeb.Web.CategoryView, :index, _) do
    "Categories - Firestorm"
  end
  defp get_page_title(_, _, _), do: "Firestorm"

This is so wordy it's not straightforward to read, and we're going to be repeating ourselves. Let's clean this up a bit. First, we'll import these two functions in all of our views:

vim lib/firestorm_web/web.ex
defmodule FirestormWeb.Web do
  # ...
  def view do
    quote do
      # ...
      # Import convenience functions from controllers
      import Phoenix.Controller, only: [
        get_csrf_token: 0,
        get_flash: 1,
        get_flash: 2,
        view_module: 1,
        action_name: 1
      ]
      # ...
    end
  end
  # ...
end
defmodule FirestormWeb.Web.LayoutView do
  use FirestormWeb.Web, :view
  alias FirestormWeb.Web.{CategoryView, ThreadView, UserView}
  @app_name "Firestorm"

  def page_title(conn) do
    view = view_module(conn)
    action = action_name(conn)
    get_page_title(view, action, conn.assigns)
  end

  defp get_page_title(CategoryView, :index, _) do
    "Categories - #{@app_name}"
  end
  defp get_page_title(_, _, _), do: @app_name
  # ...
end

This is getting nicer, but ultimately we're going to want to make it more complex and it will muddy up the LayoutView. Let's defer to a module specifically for handling our page titles:

defmodule FirestormWeb.Web.Layout.PageTitle do
  alias FirestormWeb.Web.{CategoryView, ThreadView, UserView}
  @app_name "Firestorm"

  def for({view, action, assigns}) do
    {view, action, assigns}
    |> get()
    |> add_app_name()
  end

  defp get({CategoryView, :index, _}) do
    "Categories"
  end
  defp get(_), do: nil

  defp add_app_name(nil), do: @app_name
  defp add_app_name(title), do: "#{title} - #{@app_name}"
end

defmodule FirestormWeb.Web.LayoutView do
  use FirestormWeb.Web, :view
  alias FirestormWeb.Web.Layout.PageTitle

  def page_title(conn) do
    view = view_module(conn)
    action = action_name(conn)
    PageTitle.for({view, action, conn.assigns})
  end
  # ...
end

Now we have a nice single-purpose module, so it's not that bad if it becomes large - everything in it is related to the same thing, and we can always refactor later if we'd like.

Next, let's generate a title for the category show action:

  defp get({CategoryView, :show, %{category: category}}) do
    category.title
  end

Here we've taken advantage of this action's assigns to pull the category's title into the page title. Let's rapidly write the remaining page titles for Firestorm:

defmodule FirestormWeb.Web.Layout.PageTitle do
  alias FirestormWeb.Web.{CategoryView, ThreadView, UserView}
  @app_name "Firestorm"

  def for({view, action, assigns}) do
    {view, action, assigns}
    |> get()
    |> add_app_name()
  end

  defp get({CategoryView, :index, _}) do
    "Categories"
  end
  defp get({CategoryView, :show, %{category: category}}) do
    category.title
  end
  defp get({CategoryView, :new, _}) do
    "New Category"
  end
  defp get({CategoryView, :edit, %{category: category}}) do
    "Edit #{category.title}"
  end
  defp get({ThreadView, :show, %{thread: thread, category: category}}) do
    "#{thread.title} - #{category.title}"
  end
  defp get({ThreadView, :new, %{category: category}}) do
    "New Thread - #{category.title}"
  end
  defp get({ThreadView, :edit, %{thread: thread}}) do
    "Edit #{thread.title}"
  end
  defp get({UserView, :show, %{user: user}}) do
    "#{user.username} - Users"
  end
  defp get(_), do: nil

  defp add_app_name(nil), do: @app_name
  defp add_app_name(title), do: "#{title} - #{@app_name}"
end

Now if we click around the site, we get nice titles on every page.

Summary

In today's episode, we added custom titles for each of our pages. You can use this pattern and customize it for your own needs, of course.

There's head-related data other than the title that you will likely want to be custom on various pages as you build a more complex application. In that case, it might make sense to define a more broad Metadata module, rather than PageTitle, and then maybe it makes sense to break it out into resource-specific modules. For our purposes, what we have will work fine. We'll cross that bridge when we get there.

I hope you enjoyed it. See you soon!

Resources