The FriendlyId gem lets your app work with ‘friendly’ URL slugs like ‘myapp /cities/amsterdam’. You can map the slug to the root path, but what if you have two resources with friendly slugs, and you want both of them in the root path? Recently I encountered this question, working on the website for . Here’s the barebone version of our solution. WomenWhoCode We wanted to achieve that the app can use URLs with slugs on the root level for 2 different and unrelated resources, let’s name them and . Instead of URLs like we want the slugs on root level for both resources, so that the app can use . City Flower myapp/cities/amsterdam, myapp/amsterdam and myapp/tulips Basic setup FriendlyID Before we start, a quick reminder of the basic FriendlyId setup: For Rails 5.1+ and FriendlyId version up to 5.2.1 :Before running the migration, go into the generated migration file and specify the Rails version: class CreateFriendlyIdSlugs < ActiveRecord::Migration**[5.1]** The crux of our solution is the module with the table, which is generated by the friendly_id generator (line 6^). The table has a column, and also a and . The sluggable_type is named after the resource where the slug belongs. See? That comes in handy later on! :history friendly_id_slugs slugs sluggable_type sluggable_id With FriendlyId set up, we can use the gem’s methods. Given a Thing with , we can find them the friendly way; will return our whether we pass in the slug or the . So the application can use both and . finder :something id: 123 Thing.friendly.find(params[:id]) something id /things/something things/123 For this article we use the friendly_id defaults; check the docs for tweaks and tips. Goal With the routing set up as usual with , the show view is pointed at with . But now we want the slug at the root level: . resources: flowers myapp/flowers/tulip myapp/tulip For a single controller we can do that by adding the option to the route: path # one controller root level slug# routes.rb resources :flowers, only: :show, path: '' … and then goes to the controller’s action /myapp/tulip flowers #show So far no problem. But now we want the **flowers** slug the and **cities** slug at the root level. If we add the second route declaration, for , also with , then the Rails router wouldn’t know if it should point to the flowers controller or to the cities controller. Actually: because routes are executed in the order that they appear in it will always point at the controller that is listed first. So, if the Flower routes appear before the City routes, with , it’s the controller that steps in and tries to find a _flower_ with the param, and not the city we were looking for. cities path:'' routes.rb, myapp/amsterdam Flower :slug Action plan: SlugsController The solution: instead of using the resources’ own controllers, we are going to add a new SlugsController. We will not be using the resources tables, but the friendly_id_slugs table to find the right record and render its show view (step 1). We’ll route the root level slugs to this new method (step 2). For better UI (and sanitizing?) we’ll normalize the user input (step 3). Finally, we need to make sure that slugs are unique not only for its own model, but also against the other models that share the root level slugs. For that, we’ll need a custom uniqueness validator (step 5), backed up by database constraints (step 6). Here’s the plan: Create (or ) a SlugsController with a action that uses the friendly_id_slugs table and its field to distinguish and , and to render the view. generate :show sluggable_type flowers cities Point root level slugs to the new SlugsController Normalize user input Create a custom validator for uniqueness of slugs against both models Add database constraints Step 1 - Slugs Controller finds correct record What happens here? First, we find the record in the table (L4). slug friendly_id_slugs Now that we have access to the we use the type to differentiate between the models in the method (L19). Then we use the friendly_id finder methods to set the ivar and render the view. The correct ivar is sent along, so the view can use it and we do not have to go through the resources’ own controller. sluggable_type, :render_view But, this won’t work with the current routes, so let’s fix that. Step 2 - Route slugs to the new SlugsController Here, we point the slugs to the show action in the slugs controller. The regular routes come first, to keep routes like working. If you happen to have namespaced routes, like , make sure to put those higher up in the routes file too, or the slugs controller will go looking for a flower or a city called ‘admin’. /myapp/cities /admin/flowers In order for this to work, make sure to check the original show action. Any ivars set there, or any logic performed on the ivars, won’t be available in the view. In our case, it meant that we needed to refactor the show action, and ended up with a nice and clean oneliner: The rest was moved to a view helper, a scope and instance methods in the model. @city = City.friendly.find(params[:id] Now, let’s make sure that we only accept slugs formatted just like FriendlyId formats slugs. Step 3 - Normalize user input The normalizer method strips all characters that are non-word characters, plus the because FriendlyId uses that as a word-separator. in the regex matches letters, numbers, and underscore. — \w We store the normalized string in . With the ivar set here, we don’t need the method anymore, so we skip it and change all the into (L6, and 2 times in the method). @slug_params :slug_params slug_params @slug_params render_view The routing and rendering are working. One thing left to do: validating the uniqueness of the slugs. We don’t want a called ‘amsterdam’ to be allowed the slug ‘amsterdam’ if that slug is already in use for the ‘Amsterdam’. tulip city Step 4 - Validate uniqueness against all slugs: EachValidator A regular Rails uniqueness validator in the model would ensure that the slugs for one resource be unique. But we have an extra requirement, due to the ‘double routing’ we use. Opposed to the default behaviour of FriendlyId, our flower slugs can’t be the same as an existing city slug. Therefore we’ll create a custom validator that validates uniqueness against both models. And we’ll use this validator in each model. The is the way to go for validating attributes. It requires the implementation of a method. I made the message for invalid slugs the same as the message for standard uniqueness validations.The method (L10) iterates over the array with model names. We need the model , not the model name, so we the string. EachValidator validate_each valid_slug? constant constantize Then (L13) we use a predicate method from the FriendlyId::FinderMethods module, to check if the slug exists in each model. I am not particularly happy with the , but for now, I liked it a bit better than other options, mostly for readability’s sake. return false if- statement We break out of the loop as soon as there is a match because any match makes the new slug invalid. In our case, one of the models has a lot more records than the other; that model comes last in the array (in L4). We call this validator in both models: #in flower.rb and city.rb: validates :slug, presence: true, , if: :slug_changed? slug: true is the actual call to the custom SlugValidator. It is fired only if and when the slug has changed. In the example project, that happens only on ; in a real project, that needs fine-tuning. slug:true :create Step 5 - Add database constraints One valuable lesson I learned with this issue, is to back uniqueness validations with database constraints. When two users are adding the same slug at the exact same time, the Rails validation may pass. In that case, we want the to prevent that the equal slugs both are being saved. database We already have database constraints on each model (see above, the first gist, L7 & 8). All we have to do is adding a unique index for the friendly_id_slugs table. (Note the spelling .) uniq # in terminal: $ rails g migration AddIndexToFriendlyIdSlugs slug: uniq # don't migrate yet This will create the migration file. Remove the line. add_column # in db/migrate/2017...._add_index_to_friendly_id_slugs.rb < :: [5.1] :friendly_id_slugs, :slug, :string , , : class AddIndexToFriendlyIdSlugs ActiveRecord Migration def change# remove the following line:add_column add_index :friendly_id_slugs :slug unique trueend # then run$ rails db:migrate After removing the redundant line from the change method and migrating, the following index is added to : db/schema.rb .index [ ], : , : t "slug" name "index_friendly_id_slugs_on_slug" unique true And with this last step, our root level slug system is in place. Yay 🎉 ! For the simplicity of the example project, we use the FriendlyId defaults. Do check the docs and read other examples to make the experience for your users even better. The complete sample project: https://github.com/F3PiX/slugs_example , and Tonee (twitter: @tonee). Thanks to Pablo Rivera for the inspiring story on technical posts: I used the tips to write this post. Thank you, code and text reviewers: Zassmin Jeanine Soterwood https://dev.to/yelluw/how-to-do-technical-blogging Read more: FriendlyId: https://github.com/norman/friendly_id A nice post about getting started with and finetuning friendly_id: http://vaidehijoshi.github.io/blog/2015/12/15/youve-got-a-friend-in-friendly-id/ RailsCast: Outdated, but always a pleasure: http://railscasts.com/episodes/314-pretty-urls-with-friendlyid?view=asciicast About validations and constraints: (short: ; long: ) https://stackoverflow.com/questions/2367281/ruby-on-rails-is-it-better-to-validate-in-the-model-or-the-database https://robots.thoughtbot.com/validation-database-constraint-or-both Questions or an opinion on the text or the code? I’d love to hear from you!