5

I have a number of different models in the Rails app I'm working on. I've seen a number of sites use an app-wide slug routing approach. What do I mean by this?

http://example.com/nick-oneill <-- This points to a User object
http://example.com/facebook    <-- This points to a Company object
http://example.com/developers  <-- This points to the users#index page

I'm aware of to_param and creating reader-friendly slugs within apps, however I'm not aware of an approach to have root-level slugs for a variety of objects. You can think of this being similar to Facebook's Graph API: there are different object types, but all of them exist at https://graph.facebook.com/object-id

Any insight would be much appreciated!

Nick ONeill
  • 7,341
  • 10
  • 47
  • 61
  • hmm. you probably have a table of slugs where you keep what type it is. then just add a route that checks for that type and try to redirect based on that. – jvnill Mar 13 '13 at 01:08
  • That seems a bit excessive though because now I need to make two requests to fetch the object, no? – Nick ONeill Mar 13 '13 at 01:09
  • it is. i don't know how rails will differentiate between slugs unless they're already defined or at least have some sort of pattern which can distinguish each of them. – jvnill Mar 13 '13 at 01:15
  • Check out here http://stackoverflow.com/questions/15082336/seo-friendly-urls-in-ror/15083439#15083439 – Sergey Kishenin Mar 13 '13 at 01:58
  • @SergeyKishenin that gem may work, however I don't see anywhere that explains how to use it to accomplish what I'm describing – Nick ONeill Mar 13 '13 at 16:07
  • Check out Rails quickstart https://github.com/norman/friendly_id#rails-quickstart – Sergey Kishenin Mar 13 '13 at 17:38
  • @SergeyKishenin those still contain models as prefixes (i.e. /users/username versus /username) – Nick ONeill Mar 13 '13 at 17:41
  • Well, then you can just add `slug` attribute, make it unique and create route like: `match "/:user_slug" => "users#show", as: :user`. Then call `<%= link_to @user.name, user_path(@user.slug) %>`. The same you can do for any other model – Sergey Kishenin Mar 13 '13 at 18:13
  • But you still can use `friendly_id` for slug generation – Sergey Kishenin Mar 13 '13 at 18:14
  • Feel free to tell me if I need to make all this comments as a single answer :) – Sergey Kishenin Mar 13 '13 at 18:19
  • @SergeyKishenin, if you think you know the solution, you should always put it as an answer! – Nick ONeill Mar 13 '13 at 18:49

3 Answers3

10

There may be a way to do this with freindly_id but i think the problem with friendly id is things are scoped by model.

If I wanted truely sitewide slugging I would create a slugs table with a polymorphic relationship to all my models.

Sluggable_type and sluggable_id and then a slug field with the complete permalink/slug.

+---------------------------------------------+
| sluggable_type | sluggable_id |     slug    |
|      user      |       13     |  users/john |
+---------------------------------------------+

Now i could do do a wildcard catch all route or create the routes for all my slugs at runtime and force a route refresh when a model is updated that was under this sluggable control.

routes.rb

  get "/*segments",
               :controller => 'slugs',
               :action => 'dynamicroute'

Now in your SlugsController implement a method like

def dynamicroute
  segments = params[:segments]
  slugs.find_by_slug(segments)
  slug.sluggable_type.constantize.find(slug.sluggable_id) #retrive real record
  #some magic to handle the slugged item maybe redirect to the appropriate
  #controller or somehow call the show view for that controller
end

OR

routes.rb

begin  
  Slug.all.each do |s|
    begin
      get "#{s.slug}" => "#{s.sluggable_type.demodulize.pluralize.camelize}#show"
    rescue
    end
  end
rescue
end

If you use the 2nd approach to routing make sure you call

YOUR_APP_NAME::Application.reload_routes!

After editing any slugged record to refresh the routing table.

We've had similar issues and we may try our hand at gemifying this approach.

j_mcnally
  • 6,928
  • 2
  • 31
  • 46
  • Damn, beat me to a similar answer while I was writing. It looks like your approach to routing may be more thought out than my naive one, though. – imakewebthings Mar 13 '13 at 19:18
  • we use it in production, although only for Pages but i think it should scale out like this? Its nice because you see all your slugged routes in `rake routes` – j_mcnally Mar 13 '13 at 19:19
  • @j_mcnally, do you use the first or second version in production? The second method appears WAYYYY cleaner. It becomes pretty dirty when I have to load another controller from within a controller method. One exception I think here would be potentially creating an intermediary controller called SluggedController which subclasses ApplicationController, then subclass SluggedController for other controllers. Curious to get your thoughts here! – Nick ONeill Mar 14 '13 at 18:28
  • We used to use the first, but now use the 2nd. Like i said its super nice as all our slugs show up in rake routes, the only pain in the ass is reloading routes when our slugs change, but we have that pretty nailed down. If you want you can put that in an after_filter of an abstract controller class and then subclass all your controllers that use slugging. Which would make the proccess of reloading routes after update and create super dry. Also remeber to call or add to that filter any custom methods like a tree sorting etc, where your slug might change. – j_mcnally Mar 14 '13 at 20:55
  • also you may be able to clean up all the rescues etc. just trying to be bullet proof on our site so that the app will still load if it has issues with a slug... this can be common during migrations/rakes etc. Especially during deployment. – j_mcnally Mar 14 '13 at 20:58
  • So, using the second approach, once you land in the controller "show" method, how do you most reliably retrieve the original slug (e.g. the path that matched the route) and turn that into a lookup for the target object that you are actually wanting to display? Also, how do you create links to your slugged model objects? Simply doing url_for(my_user) doesn't work... – Tinynumbers Sep 26 '13 at 16:58
  • 1
    Inside of the #show method you can just slug = Slug.find_by_slug(request.path) and then slug.sluggable considering your polymorhic relation is setup properly. your url should already be encoded by the slug. if you have a one-to-one with and object and a slug, you can probably my_user.slug.slug – j_mcnally Sep 26 '13 at 17:12
2

I'd probably approach this as follows, at least as a first pass:

  • Use friendly_id or similar to generate slugs for each model involved
  • Hook up a catch-all route for /([-_a-zA-Z0-9]+) and point it to something like EntitiesController#show
  • Hook up a higher priority route for /developers pointing to Users#index
  • In EntitiesController#show:

    @entity = User.find(params[:id]) or Company.find(params[:id]) or raise ActionController::RoutingError.new('Not Found')

  • Then, based on the type of entity you've got:

    render "VIEW_PATH_BASED_ON_ENTITY_CLASS/show"

I would also order the finds from most to least frequently accessed (guess first, then use data later to tweak the order).

Finally, probably obvious but make sure you're indexing the slug column in each table since you'll often be doing multiple finds per request.

FWIW I'd love to know a better way to approach this as well; this is simply how I'd attack the problem initially.

Kyle
  • 1,054
  • 2
  • 14
  • 27
  • Thanks for the note Kyle! I've upvoted your answer but am waiting to see what else I get ... trying to find the best approach here :) – Nick ONeill Mar 13 '13 at 18:48
0

My first reaction is to create a new Slug model. This model would have a polymorphic belongs_to:

belongs_to :sluggable, :polymorphic => true

At the least this table would have:

  • value - Or some better name than this. The value of the slug itself.
  • sluggable_type and sluggable_id - The polymorphic foreign keys.

Your company, user, etc. models models would just have a slug:

has_one :slug

This gives us a few advantages off the bat:

  • It's now easy to make a unique constraint on the slug value. If the slug were kept as a property of all the different sluggable models, your unique constraint would have to check all the other sluggable tables for uniqueness. Makes for a bad time.
  • The routing is simple enough, since you can use a normal resource route off of the root level namespace. You would want to keep it low/at-the-end of the route file though so other more specific routes take precedence. Edit: This routing is basically the first routing method j_mcnally suggests.
  • All the logic for a slug, like what makes a valid slug, is kept in this one model. Good separation of concerns instead of polluting say a User model. Especially if the slug rules are the same for everyone as they are here.

In terms of how the controller would work, I'd go with what Kyle said and key off the sluggable_type field to find the view you want to render.

imakewebthings
  • 3,388
  • 1
  • 23
  • 18