Implementing page-specific titles in Phoenix

SeriesPhoenix on Rails clock3 min read

Every website needs informative, SEO-friendly page titles that change during navigation. Here's how to have them in Phoenix.

Note: Although page title is a most common case, this article applies just as well to implementing other page-specific attributes in the layout, like descriptions and favicons.

Option A: Assigns

The most obvious means for having per-page title seems to be via conn.assigns. You could either explicitly call the put_assign in controller (or plug) or just pass the page_title into the render call of specific action, like this:

lang:elixir
defmodule CloudlessStudio.ArticleController do
  def index(conn, _params) do
    # (...)

    render conn, page_title: "Articles - Cloudless Studio",
                 articles: articles
  end

  def show(conn, %{ "id" => id }) do
    # (...)

    render conn, page_title: "#{article.title} - Cloudless Studio",
                 article: article
  end
end

Of course, you'd probably move the "#{...} - Cloudless Studio" suffix to some function in LayoutView in order to stay DRY. Still, you're putting view code into your controller. Also, if you need to pass more page-specific properties, like description or favicon, the amount of assigns and the view code in general will become huge and overshadow the real meat of the controller.

Option B: Per-resource views

Some time ago, I've found the article Using page-specific page titles in the Phoenix framework. It explains the following two methods for achieving page-specific titles:

  1. Using try, apply and catch in LayoutView to dynamically invoke page_title (or any other) function in specific resource's view in order to get the title.
  2. Using render_existing in LayoutView to invoke and pattern-match the render function in specific resource's view, dedicated to rendering the title

Both methods try to place the actual title generation code in the view layer which in my opinion is A Good Thing™ and an improvement over option A as this indeed is a view layer's concern. 

They also struggle (in a somewhat hackish way) to delegate the responsibility for generating title code to specific resource's view. At first, this also seemed like a good idea to me. But sometimes the sole fact that you can't achieve something easily may indicate that you're doing it wrong. And indeed, after giving it a thought I'd say that page title code naturally belongs to the LayoutView responsible for rendering everything in <head>. After all, per-resource views only handle the yielded part in the <body> so why would we force more responsibility upon them?

Option C: Dedicated modules (recommended)

So after rejecting options A and B, we're still in the layout view, which will probably grow quickly. That's why the solution that I'm going with in my projects is simply to have single module for page title, description or icon code and to import such module into layout view. For example:

lang:elixir
defmodule CloudlessStudio.PageTitle do
  alias CloudlessStudio.{ HomeView, ProjectView, ArticleView }

  @suffix "Cloudless Studio"

  def page_title(assigns), do: assigns |> get |> put_suffix

  defp put_suffix(nil), do: @suffix
  defp put_suffix(title), do: title <> " - " <> @suffix

  defp get(%{ view_module: ProjectView }), do: "Works"
  defp get(%{ view_module: ArticleView, view_template: "show.html", article: article }) do
    article.title <> " - " <> article.series
  end
  defp get(_), do: nil
end

Here's what is happening here:

  • the only public function, page_title, consumes assigns and assembles the page title 
  • in get, we pattern match against specific view or template to target controllers and actions
  • we're also decomposing assigns in order to get any data needed to assemble the title
  • PageTitle module is a perfect place for stuff like the @suffix attribute, too

All that's left is to import this module from LayoutView and to call page_title(assigns) in the application template (usually app.html.eex).

This solution feels most Elixirish to me, because it's simple, elegant and explicit. It creates a logical place for page title code to live in, it's not hackish at all and it cleverly uses pattern matching. Also, in case of project growth, it's going to be easy to split this module into smaller ones. And it will be possible to split in any fitting way, not just by controllers, like in option B. 

Like usually, please leave a comment and let me know which approach do you like the most or if you've found another one.