Click To Skip To Main Content
 
Return to blog

Elixir + Phoenix: How to Remove the Trailing Slash From Your URLs

A brief tutorial to help prevent URL duplication and improve your website's SEO.

Created 2023-07-26. Modified 2024-04-06.

Example code written and tested using Phoenix v1.7.7

Table Of Contents

  1. Introduction
  2. Why Do I Need This?
  3. Show Me the Code!
 

Introduction

Phoenix is a versatile framework. With just a few lines of code, you can easily remove that pesky trailing slash from the end of your URLS. This helps to ensure that your website's SEO is up to snuff and prevents unnecessary duplication of URLs.

For example:

  • /your/page/ -> /your/page
 

Why Do I Need This?

By default, Phoenix will return the same result whether or not a URL contains a trailing slash, effectively creating 2 possible URLs for a given resource. One consequence of this is that a search engine can index both pages, even though they only represent a single resource.

From a SEO perspective, this is not ideal. Search engines prioritize content based on how many sites link to a given page. The "link juice" for a given page can be diluted if you have some links pointing to a URL with a slash, while other links point to a URL with no slash. To maximize the SEO for a given page, a user should only be able to access it from a single URL.

Furthermore, it makes sense that a resource should only be accessible from a single URL. In most cases, there is no good reason to allow access to a given resource from more than one "canonical" URL.

The following code will allow your Phoenix router to redirect all URLs with a trailing slash, to the same resource without the trailing slash. Any user that attempts to browse to the incorrect (trailing slash) URL will be automatically redirected to the correct one.

 

Show Me the Code!

Create the Plug

lib/your_project_web/plug.ex

defmodule YourProjectWeb.Plug do
  @moduledoc """
  Your project's custom function plugs.
  """

  use YourProjectWeb, :controller

  @doc """
  If a non-root URL ends with a slash '/', do a permanent redirect to a URL that
  removes it.
  """
  def remove_trailing_slash(conn, _opts) do
    if conn.request_path != "/" && String.last(conn.request_path) == "/" do
      # trailing slash detected: return a permanent redirect to a URL without
      # the trailing slash, and halt the current request
      conn
      |> put_status(301)
      |> redirect(to: String.slice(conn.request_path, 0..-2//1))
      |> halt()
    else
      # no trailing slash detected. the request will continue down the plug
      # pipeline
      conn
    end
  end
end

Use Our New Plug in the Router

Let's add our newly-created plug to the router's plug pipeline:

lib/your_project_web/router.ex

defmodule YourProjectWeb.Router do
  use YourProjectWeb, :router

+ import YourProjectWeb.Plug

  pipeline :browser do
    plug :accepts, ["html"]
+   plug :remove_trailing_slash
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {YourProjectWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  # ...

end

That's it! Now, any URLs with a trailing slash will issue a permanent redirect (301) to the same URL, but without the slash.

Write Some Tests

Let's make sure our new plug works as intended:

test/your_project_web/plug_test.ex

defmodule YourProjectWeb.PlugTest do
  @moduledoc false

  use YourProjectWeb.ConnCase

  describe("remove_trailing_slash/2") do
    test "returns successful response when URL does not have a trailing slash", %{conn: conn} do
+      # TODO: ensure that you use a path that actually exists on your router
-      test_path = ~p"/your-path"

      conn = get(conn, test_path)

      # returns successful response
      assert html_response(conn, 200)
    end

    test "redirects to expected route when non-root URL has a trailing slash", %{conn: conn} do
+      # TODO: ensure that you use a path that actually exists on your router
-      test_path = ~p"/your-path/"
-      expected_path = ~p"/your-path"

      conn = get(conn, test_path)

      # returns permanent redirect to expected path
      assert conn.status == 301
      assert get_resp_header(conn, "location") == [expected_path]
    end
  end
end

Run mix test and the tests should pass (as long as you used a URL that exists in your router).


Updated 2023-08-05: Update example code so that the step count is specified explicitly when slicing the string. This fixes a deprecation warning in Elixir v1.16.0.


This blog post is licensed under CC-BY 4.0. Feel free to share and reuse the content as you please. Please provide attribution by linking back to this post.


Return to blog