#27: Moving to Elixir Part 3

Published December 13, 2017 6m 46s

Elixir 1.4.5

Phoenix 1.3

Slugger 0.2.0

Plug 1.4.3


In part 2 we got our site displaying some basic data about our car listings. In part 3 let’s focus on the URL structure for our cars.

Currently we are using the car’s primary key column - car_pk - in the URL.

As a part of this migration let’s make our URLs more search engine friendly by displaying the make, model, and year in the URL as well.

Let’s first open up our car.ex module. And here we’re specifying the key to use - car_pk. Let’s expand on this and create our own implementation of the Phoenix.Param protocol.

Let’s go to the bottom of our module, and here we’ll define the implementation for our Car module. Now the Phoenix.Param protocol expects a to_param function to be implemented. So let’s do that here.

And it will receive the car struct, so let’s pattern match to get the data we want. We’ll get the primary key, year, make, and model.

And then inside the function we’ll build our new URL structure or slug. We’ll want it to read year, make, model, ID, all lowercase, with a hyphen between each word.

Now we could write our own function to build our slugs, but let’s reach for a bit more of a robust solution and bring in the Slugger package.

We’ll open our ‘Mixfile’ and add it to our dependency list:

mix.exsdefp deps do
  [
  …
  {:slugger, "~> 0.2.0"}
  …
  ]
end

Then we’ll download our new package:

$ mix deps.get

And then start our server:

$ mix phx.server

Now back in our car.ex module we’ll call Slugger.slugify_downcase

And then we can pass in a string with the data we want for our new URL and it will handle down-casing and joining all words with a hyphen by default.

lib/dealership/listings/car.ex…
defimpl Phoenix.Param, for: Dealership.Listings.Car do
  def to_param(%{car_pk: id, car_Year: year, car_Make: make, car_Model: model}) do
      Slugger.slugify_downcase("#{year} #{make} #{model} #{id}")
  end
end

Now let’s go to our browser, and using our new format, let’s see if it returns a car: http://localhost:4000/cars/2012-chevrolet-silverado-1500-5693

And we get an error. It’s trying to lookup our car with the car_pk but now we’re passing it our slug.

We need to update this to parse out the data we want. We’ll open our car_controller.ex and update our show action. Let’s first change id to slug, since this is now what’s being passed in. Then let’s create a function that will parse our slug and return an the id.

Let’s create a new private function called id_from_slug. It will take our slug, then split all the words. And we know our id will always be the last item in our list, so we can return it with List.last.

Great, now let’s call our new function to get the id.

Now we may be tempted to get our id like this: car = Listings.get_car!(id_from_slug(slug))

But Elixir provides a better way write this - the pipe operator. We can take our slug, pipe it into our id_from_slug function and then the result of that into Listings.get_car!.

lib/dealership_web/controllers/car_controller.ex…
  defp id_from_slug(slug) do
    slug
    |> String.split("-")
    |> List.last()
  end

  def show(conn, %{"id" => slug}) do
    car = slug
          |> id_from_slug()
          |> Listings.get_car!()
    render(conn, "show.html", car: car)
  end
...

With that let’s go back to the browser http://localhost:4000/cars/2012-chevrolet-silverado-1500-5693 and perfect our page loaded - and you can see all our links are updated to the reflect our new URL.

Now while our new links are working, we do have one thing we need to address. Since we’re working with a legacy application, the old URLs have been indexed by search engines.

We need to create a way to forward traffic from our old URLs, to our new ones.

The old route was /inventory so for example, an old URL, /inventory/5693 would need to be redirected to /cars/2012-chevrolet-silverado-1500-5693.

Let’s check out the documentation for the Phoenix.Router.get function here.

We see that our router is just using plugs. So let’s create our own plug to handle a our redirect. In our router, let’s use the get function, and we’ll give out our old car route - /inventory/:id.

Then let’s use a plug we’ll call LegacyRedirect

And finally we’ll pass in the route we want to forward to

lib/dealership_web/router.ex  scope "/", DealershipWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/cars", CarController
    get "/inventory/:id", LegacyRedirect, forward_to: "/cars"
  end

Now let’s define our plug. We’ll create a new file, legacy_redirect.ex

And we’ll define the module.

Now this is going to be a module plugs, so we two functions we need to implement - init and call.

init takes any options we provide the plug. In this case we’ll just return them. call then takes two arguments - the connection, and the options returned by init, which will be our new path. Let’s go head and pattern match on the options to grab our new path.

Now we need to do a couple of things here.

We’ll first need to lookup the car. Then we need to create our slug from the car and once we have the slug, we need to redirect to our new path with a status of 301, since this is a permanent change.

Let’s start by building our slug. We’ll create a new function named build_slug that will return our slug from our connection.

Now let’s implement it. We’ll create a new a private function - build_slug.

And since we’re passing the connection, let’s pattern match to get the params, since we’ll need them to lookup the car. Inside the function, we can use the ID to lookup our car.

We’ll start with the ID from our params. Then let’s use the Listings.get_car! function to lookup our car.

Now that we have our car, we can use it to build our slug. Let’s pipe it into a new function called url_structure that will be responsible for returning the structure we want for our slug. We’ll make it a private function that will take our car as an argument. Then we’ll build a string from it that has our data in the order we want it.

Then with our structure, we’ll pipe it into Slugger.slugify_downcase. We’ll also need to alias Dealership.Listings so let’s add that.

And back in our call function we’ll handle our redirect. Let’s start with our connection and create another pipeline. We’ll take the connection and let’s first set the status with the Plug.Conn.put_status function.

This will return a plug that we can then pipe into Phoenix.Controller.redirect to redirect to our new url. We’ll create our new url by joining our new_path with our slug. Finally, let’s call Plug.Conn.halt to stop any other plugs from called.

lib/dealership_web/legacy_redirect.exdefmodule DealershipWeb.LegacyRedirect do
  alias Dealership.Listings

  def init(opts), do: opts

  def call(conn, [forward_to: new_path]) do
    slug = build_slug(conn)

    conn
    |> Plug.Conn.put_status(301)
    |> Phoenix.Controller.redirect(to: "#{new_path}/#{slug}")
    |> Plug.Conn.halt()
  end

  defp build_slug(%Plug.Conn{params: params}) do
    params["id"]
    |> Listings.get_car!()
    |> url_structure()
    |> Slugger.slugify_downcase()
  end

  defp url_structure(car) do
    "#{car.car_Year} #{car.car_Make} #{car.car_Model} #{car.car_pk}"
  end

end

With that let’s test it out by going to an old url in the browser:http://localhost:4000/inventory/5706

And great we were redirected to our new structure: http://localhost:4000/cars/2014-volkswagen-passat-5706 and looking at the logs we see the 301 status was sent.

Now let’s test an old route that doesn’t exist: http://localhost:4000/inventory/1111 and a car was not found and a 404 status was returned.

Ready to Learn More?

Subscribe to get access to all episodes and exclusive content.

Subscribe Now