Using page-specific page titles in the Phoenix framework

Written using Phoenix 0.13.1

One common task in web apps is setting a custom page title for each URL - your homepage should have a different title than a page listing out all of your projects (for example), which should have a different title than a page showing details of a single project.

There isn’t a built-in way to do this in Phoenix, but after much reading around forums, GitHub issues, etc. I hit on two potential ways to do it.

Both involve using a custom page title call in application.html.eex:

<!DOCTYPE html>
<html lang="en">
<head>
<title><%= page_title(@conn, assigns) %></title>
...

Both methods also require you to make the action_name/1 controller helper available to your views. You can do this in web/web.ex by adding the action_name method to the following line:

defmodule Ticketee.Web do
...
def view do
quote do
...
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1, action_name: 1]
...
end
end
...
end

But the two methods differ in how I’ve defined that page_title/2 method.

Method 1 - try/apply/catch

This leads to a nicer interface for views, at the expense of a hacky try/catch block in the page_title/2 method.

In the LayoutView, which is used by application.html.eex, define the following methods:

defmodule Ticketee.LayoutView do
use Ticketee.Web, :view
def page_title(conn, assigns) do
try do
apply(view_module(conn), :page_title, [action_name(conn), assigns])
rescue
UndefinedFunctionError -> default_page_title(conn, assigns)
end
end
def default_page_title(_conn, _assigns) do
"Ticketee"
end
end

This will call the page_title/2 method in the view module used by the specific page template - eg. for ProjectController.index/2, it would use ProjectView. It passes the page_title/2 method the name of the current action, and the assigns data, in case you need it for generating the page title.

In the view modules, you can then define some really simple methods to set the page titles:

defmodule Ticketee.ProjectView do
use Ticketee.Web, :view
def page_title(:show, assigns), do: assigns[:project].title
def page_title(_, _), do: "Projects"
end

Alternatively, if you don’t like the pattern-matching style, you could have a single page_title/2 method and use a case statement inside it.

I think this is a really elegant solution, that gives a really nice interface where it matters the most - in the individual view modules. The downside is, of course, that hacky try/rescue needed to provide a fallback page title, in case the view module doesn’t define the page_title/2 method. If there’s a nicer way to do that, I’m all ears! Please leave a comment below!

Method 2: render_existing

This method leverages the render_existing method provided by Phoenix, which replicates the content_for method familiar to those of us coming from Rails. You can use it to render templates specific to a template or controller, or as I’ve used it here, pattern-match a method call.

In the LayoutView, you can define the following page_title/2 method:

defmodule Ticketee.LayoutView do
use Ticketee.Web, :view
def page_title(conn, assigns) do
render_existing(view_module(conn), "page_title", Dict.put(assigns, :action_name, action_name(conn)))
|| default_page_title(conn, assigns)
end
def default_page_title(_conn, _assigns) do
"Ticketee"
end
end

This calls render/2 in the relevant view module, passing the first argument of “page_title”, and the second argument of the assigns data, combined with the current action name.

In the view module, you can then pattern-match the render/2 call with the following method:

defmodule Ticketee.ProjectView do
use Ticketee.Web, :view
def render("page_title", assigns) do
case assigns[:action_name] do
:show -> assigns[:project].title
_ -> "Projects"
end
end
end

If the view module doesn’t define the render("page_title", _) method, then Phoenix will fall back to looking for a template with the name page_title, and if there isn’t one defined, render_existing/3 will just return nil, meaning the fallback will be used. It’s likely a more “Phoenix-y” way to do it, using Phoenix’s built-in tools, but I think the interface it provides is nowhere near as nice.

Summary

I’m only learning both Elixir and Phoenix, and undoubtedly there will be better ways to implement what I have described above. I think I’ll be moving forward with the first solution in my practice Ticketee app, while still looking for a nicer solution to get rid of that try/catch clause.

Side note: Yes, I am rebuilding Ticketee from Rails 4 in Action as practice - you can see my progress here!

← Home