#27: Moving to Elixir Part 3
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.