Profile picture of Aswin Mohan

Aswin Mohan

handcrafted by someone who loves code (mobile, frontend, backend), design and life.

I'm always up for meeting new people and sharing ideas or a good joke. Ping me at hey@aswinmohan.me and let's talk.

2021-Dec-18

SEO Friendly Persistent URL Slugs in Elixir Phoenix

When building out a platform it’s a great idea to let your primary outside world resource be reachable via slugs in the URL. Take the case of IndiePaper, the main resource is books, rather than letting Authors share a link with the default id https://indiepaper.me/books/06c9fa8f-19b3-4fa1-bcb7-24a2ee2e42f7, it’s best to have them share a URL with the Title in it https://indiepaper.me/books/indiepaper-building-the-future-of-publishing. This is better for SEO and readabilty.

Let’s say the Authors shares this link on their Social Media and blog posts. Months down the line the Author want to change the title to something else, but changing the title would change the URL and that would render all the traffic coming from the old links hit 404. We need URLs to point to the same resource, even if the slug changes and we’re going to learn to implement that in Elixir Phoenix.

Phoenix Params

Let’s say you have an project where Authors can post Books for sale. You will have a Book schema, and a Books context. You have the /books/:id route setup to go to the show action in your BookController.

When you want to link to a show page, you use the Routes helper.

<%= link to: Routes.book_path(@conn, :show, book) %>

But how does it resolve the entire book to a single id ? Enter Phoenix Params. Phoenix Params is a protocol to convert data structures to params usable in URLs. When used with Schemas the default :id parameter is used. To use a slug with our URLs we have to implement our own custom behaviour using this protocol, so when given a book rather than extracting :id, it will extract our :titled_slug.

Add this behaviour that implements the protocol to end of books/book.ex file, and add the to_slug/2 function to Book. Be sure to add Slugify your mix.exs file {:slugify, "~> 1.3"}, so you can use Slug.slugify/1.

def YourApp.Books.Book do
  use Ecto.Schema
  import Ecto.Changeset

  ...

  def to_slug(id, title) do
    "#{Slug.slugify(title)}-#{id}"
  end
end

defimpl Phoenix.Param, for: IndiePaper.Books.Book do
  def to_param(%{id: id, title: title}) do
    IndiePaper.Books.Book.to_slug(id, title)
  end
end

What this does is that the next time you call Routes.book_path(@conn, :show, book), it produces /book/title-slugified-UUID-ID-OF-BOOK instead of book/UUID-ID-OF-BOOK. Since UUID’s have a consistent length we can add them at the end, so the title is more prominent in the URL. If you’re using Integer ID’s there’s a section in the bottom for that.

One half of the job is done, we can now generate the slug from the schema. Now let’s see how to use that in our URLs.

Open up your router.ex file and where you are defining your book routes, define the param as slug. This is done so you can easily parse out the slug from the url.

# If you're using Vanilla Phoenix
resources "/books", BookController, only: [:show], param: "slug"

# If you're using LiveView
live "/books/:slug/", BookLive, :show

In your show action in your BookController, you can get the slug from the params.

def show(conn, %{"slug" => book_slug} = _params) do
    book = Books.get_book_from_slug!(book_slug)
    ...
end

Now we have to implement the function in Books Context which will parse out the :id from the slug and return the book. Open books.ex and insert this.

def get_book!(book_id), do: Repo.get!(Book, book_id)

def get_book_from_slug!(slug) do
    id = String.slice(slug, -36, 36)
    get_book!(id)
end

Since we know the id is going to be 36 characters long, we parse it out from the back of the String using the String.slice method. We get the id and pass it into get_book! which is a standard context function to get the book.

Since we discard the title from the slug and only query using the id, we can be sure that even if the title is changed the old and the new URLs still point to the same book. That is how folks we implement persistent Slugified URLs in Elixir Phoenix.

Using Integer IDs

If you’re using the default auto-increment ids, we have to do some minor changes. Since the length of auto-increment ids can change, we cannot append them to the back but only to the front. This affects the slug creation and parsing step only.

def to_slug(id, title) do
    "#{id}-#{Slug.slugify(title)}"
end

Parse out the first string upto the first - to get the integer id of the resource.

def get_book_from_slug!(slug) do
    [id |_] = slug |> String.split("-")
    get_book!(id)
end

That is how you can implement persistent URLs in Phoenix. Hope you like it and head on over to https://indiepaper.me to see it in action, and a big thanks to https://hashrocket.com/blog/posts/titled-url-slugs-in-phoenix for the initial ideas.

Happy Hacking Alchemist :D