While articles with the entire feature development process can be attractive to a couple of visitors, it appears that How-To's for a specific topic is way easier to find in the search engines. I'll keep posting project-related articles to share some product/discovery cases and focus more on guides for tech topics.
This time let's talk about slugs in the Phoenix framework. Slug is the part of the URL that uses human-readable keywords to identify a page. Slug is the first thing you want to support for blog post URLs, public profiles, FAQ articles, etc.
Generators of the Phoenix framework use IDs by default, and you have to let the framework know about your intention to use slugs instead.
So let's see the steps required to use slugs instead of IDs in your URL.
Basic slug support
First, define a method the app will use to search your objects using the slug, name, title, or any other field of your choice.
You can put this near get_organization!
method that the CRUD generator created for you.
def get_by_slug!(slug), do: Repo.get_by!(Organization, slug: slug)
Now replace get_organization!
calls in all live/organizations methods that you'll find (show, edit, delete actions should use this to find an organization by id
) with brand new get_by_slug!
call.
It won't work yet since we need to tell Phoenix
that we changed a key parameter from id
to slug
. Add this at the top of your organizations
schema to achieve this.
@derive {Phoenix.Param, key: :slug}
Read more about Phoenix.Param protocol here.
We have covered the happy path scenario, where users click URLs generated by the framework, but we should also consider users who can type URLs in their browser window.
Handle MixedCase uniqueness
My organization slug is Fluxdash.
I still want to enter fluxdash
and get the same results. We also wish to treat slugs' uniqueness despite CaseSelection.
To get this done, we need to create a new unique index that will apply the lower()
function to the passed slug.
Let's generate new migration for this.
mix ecto.gen.migration add_organization_slug_unique_lower_index
...
create index(:organizations, ["lower(slug)"], unique: true)
If you're here from the first post where we created the organization's table, you should also drop the unique index that we added at the beginning.
drop_if_exists index("organizations", [:slug], slug: :organizations_slug_index)
Now, as we also got a new index slug, we must reflect that in our constraint validation in the changeset; let's put the new index slug here.
...
|> unique_constraint(:slug, slug: :organizations_lower_slug_index)
And now we should update our get_by_slug!
function to use the index, and downcase input string to perform case agnostic searches.
def get_by_slug!(slug), do: Repo.one!(from o in Organization, where: fragment("lower(?)", o.slug) == ^String.downcase(slug))
Now we can use show/edit/delete with organization slugs instead of IDs.
If you need to generate slugs for more complex scenarios, such as blog titles without strict format requirements, take a look at the https://github.com/sobolevn/ecto_autoslug_field library, which can generate slugs based on the existing field.
Update routes to use slug
param name
If you have both types of routes, with id
s and slug
s, it makes sense to adjust router with expected attribute name to avoid confusions.
# lib/fluxdash_web/live/organization_live/show.ex
def handle_params(%{"slug" => slug}, _, socket) do
...
end
# routes.ex
live "/organizations/:slug", OrganizationLive.Show, :show
PS. Originally posted as part of Fluxdash Organizations series.
Top comments (0)