While Vaporware is primarily a Rails and React shop, we're always exploring new technologies that'll help our clients create the best possible product. Lately, I've been doing a deep dive into Elixir and came across the need to have some SEO friendly titles for a blog-like feature. There's a few resources out there already but none that I could find in the new Phoenix 1.3 world. We're going to start a new app from scratch but for the sake of brevity I'm only going to talk about the title-relevant parts.
I assume you already have a Phoenix 1.3 environment up and running. If you don't... start here.
Getting started
We'll begin by starting up a new app. In your terminal type
mix phx.new blog
cd into your new blog
directory. First we'll create the database and then let's create a Blogs
context where we'll keep our posts
table. To read more about contexts I'd recommend checking out the official docs.
In your terminal type
mix ecto.create
When that's done
mix phx.gen.html Blogs Post posts title:string body:text
This scaffolds pretty much everything that you need to get a basic blog going which is great for us!. Don't forget to add
resources /posts, PostController
to your lib/blog_web/router.ex
file. We can now run mix ecto.migrate
and when that's done mix phx.server
and we'll have a fully working blog! Navigate to http://localhost:4000/posts
to try it out!
Friendly URLs
After you've created a post or two you can click "Show" and you'll get to the post's specific page. Check out the URL and you'll see http://localhost:4000/posts/1
- while this is perfectly fine and functional, it's not the most appealing looking thing. Just like titles, URLs are an important part of search engine optimization. In addition to rankings, more readable URLs are more likely to be clicked when showing up in search result and also look much better when being shared on places like Twitter.
We're going to use a hyphenated version of our title for our URLs. You'll often see these referred to as slugs
. First, we need to create them (we'll also reset the db to delete the old posts).
In your terminal...
mix ecto.reset
mix ecto.gen.migration add_slug_to_posts
Open up that migration file
// ...
def change do
alter table(:posts) do
add :slug, :string
end
create unique_index(:posts, [:slug])
end
We add a column called slug
that's a String - that part is self explanatory. We also want to create a unique index because we're going to be doing our Post
queries by slug. We query resources by url parameter (e.g. /posts/1
will find the Post with id = 1). Because we're replacing id with a slug in the url, we'll also need to replace our queries with a slug. Let's do that now.
# lib/blogs/post.ex
Add this to the bottom of the post file
defimpl Phoenix.Param, for: Blog.Blogs.Post do
def to_param(%{slug: slug}) do
"#{slug}"
end
end
The above code implements a Protocol in Elixir. I'm not going to go in depth with Protocols (you can here) but basically they're elixir's way of handling polymorphism. This allows us to implement the Param.to_param
protocol returning the Post's slug instead of it's id. We've now stored the slug in the database and told Phoenix to use it in to_param
. Now we need to actually transform our title into a slug and cast it in our changeset.
# lib/blogs/post.ex
schema "posts" do
field :title, :string
field :body, :string
field :slug, :string # add this line
timestamps()
end
@doc false
def changeset(%Post{} = post, attrs) do
attrs = Map.merge(attrs, slugify_title(attrs)) # merge the slug with attrs
post
|> cast(attrs, [:title, :body, :slug]) # make sure to cast :slug
|> validate_required([:title, :body])
end
transform title into our slug
defp slugify_title(%{"title" => title}) do
slug =
title
|> String.downcase
|> String.replace(~r/[^a-z0-9\s-]/, "")
|> String.replace(~r/(\s|-)+/, "-")
%{"slug" => slug}
end
need to have a function w/o title for our :new controller method
defp slugify_title(\_params) do
%{}
end
...
Whew that was a lot but really it was pretty simple. We wrote a slugify_title
function that takes our existing title; makes them all lowercase letters, then gets rid of all invalid characters and separates the words with a hyphen. We then merge that new map with our existing attrs
map and cast that to our post and viol a - we have our title-as-slug stored in the db!
Using our new slug
As we mentioned earlier, Phoenix uses what's in the route param to find our posts. We can see this if we open up our PostController
, let's open that up and have pattern match our new slugs instead of the post's id.
# lib/blog_web/controllers/post_controller.ex
...
def show(conn, %{"slug" => slug}) do
post = Blogs.get_post!(slug)
...
end
def edit(conn, %{"slug" => slug}) do
post = Blogs.get_post!(slug)
...
end
def update(conn, %{"slug" => slug}, %{"post" => post_params}) do
post = Blogs.get_post!(slug)
...
end
def delete(conn, %{"slug" => slug}) do
post = Blogs.get_post!(slug)
...
end
That was easy! All we did was replace all the instances of id
with slug
. If you're a vim user... you can do :%s/id/slug/g
and be in a couple seconds! Notice how we're changing what we pass to Blogs.get_post!
so we'll need to update that function to use slugs too. We'll find that in our Blogs context file.
# lib/blogs/blogs.ex
def get_post!(slug), do: Repo.get_by!(Post, slug: slug)
We're almost done, the last thing we need to do is tell the router what it parameter it should expect to use... we already know what that is.
# lib/blog_web/router.ex
...
resources "/posts", PostController, param: "slug"
...
That's it! Now when you create a new post you should see our new SEO friendly title in the URL! All the routes work around that now rather than the boring id
.
SEO friendly conclusion
Hopefully you've learned how to slugify your URLs and maybe even a bit about Phoenix in general. You can find all a repo with this example app on my Github.
Feel free to hit me up on Twitter @carljreyes if you have any questions or just want to chat... and/or if you want Vaporware to get started on building out your app (maybe in Elixir)!