Elixir + Phoenix: How to Remove the Trailing Slash From Your URLs
Created 2023-07-26. Modified 2024-04-06.
Example code written and tested using Phoenix v1.7.7
Table Of Contents
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.