Easy and flexible breadcrumbs for Rails

Most projects I work on require some implementation of breadcrumbs. Unfortunately most solutions that I found take the very simplistic approach: they split the URL and present each part as a link. I really have a hard time imagining that this can be sufficient for any non-trivial app.

I also found session-based implementation, but according to the above mentioned Wikipedia article classification these are path-style breadcrumbs, while I need location-style with a little bit of attribute-style mixed in where needed. That is, the breadcrumbs should normally reflect the page’s location in some (virtual) website hierarchy. Sometimes, while browsing things that are naturally hierarchical (like forum threads/posts, category trees and so on), the breadcrumb trail should also reflect this hierarchy. Read on to see my take on this problem.

The requirements

First, let’s formulate the requirements for the breadcrumbs solution.

  1. It should be a list of links with descriptions, except for the last element, which should not be clickable because it represents current page.
  2. It should have syntax that is easy to use, ideally something like: add_breadcrumb 'Description', some_path
  3. It should allow us to add arbitrary number of elements, both statically and dynamically.
  4. The static parts should be similar to filters:
    1. declared at one place (e.g. at the top of the controller) not littered around actions or views,
    2. inheritable (so you can declare home link only once, in your ApplicationController),
    3. with easily recognizable order,
    4. and support for filter options like :only or :except.
  5. The dynamic parts can be added at any time during the request processing, from actions, filters of views, if necessary.

Looks like a lot of hard work? Maybe. But my priority was flexibility and ease of use, not necessarily ease of implementation. I want to be able to do things like this:

Simple example

class ApplicationController < ActionController::Base
  add_breadcrumb 'Home', '/'
  ...
end  
class ThingsController < ApplicationController
  add_breadcrumb 'Things', 'things_path'
  add_breadcrumb 'Create a new thing', '', :only => [:new, :create]
  add_breadcrumb 'Edit a thing', '', :only => [:edit, :update]
  
  def show
    @thing = Thing.find(params[:id])
    add_breadcrumb @thing.name, ''
  end
  ...
end

With this setup, we get breadcrumbs like Home > Things for things index, Home > Things > Create a new thing for new thing or Home > Things > some name for show action.

Notes

  • We need to quote *_path and *_url (like this: 'things_path') when outside of actions due to the way these methods are implemented in Rails. I stretched requirement #2 here but I think this is really only a minor inconvenience. When you forget to do this, you'll get an undefined method error. The quoted string will be eval-ed when the request is processed, which is of course not the cleanest solution, but works for me.
  • When the added element is guaranteed to be the last, we don't have to provide the URL for it.
  • Although it may seem that :new is sufficient for the new thing form, we need to add :create to have the breadcrumb defined when the validation fails and the form is redisplayed (same for :edit and :update, respectively). This may look like a redundancy but that's how Rails works. Alternatively you can add the breadcrumb from the new.html.erb template or inside the :create action. But I prefer to have them declared in one place if only possible.
  • If you use add_breadcrumb in your ApplicationController, you must do it after the methods are defined (it should be under those methods).

Dynamic example

Now for something a little bit more dynamic, let's assume we have a hierarchy of categories and while browsing a category we want the breadcrumb to show all the ancestor categories:

class CategoriesController < ApplicationController
  add_breadcrumb 'Categories', 'categories_path'
  
  def show
    @category = Category.find(params[:id])
    set_breadcrumb_for @category
  end
  ...
private
  def set_breadcrumb_for cat
    set_breadcrumb_for cat.parent if cat.parent
    add_breadcrumb cat.name, "category_path(#{cat.id})"
  end
end

This lets us have breadcrumbs like Home > Categories > Root > Parent > Child for tree-like category hierarchies. The set_breadcrumb_for method traverses all the parents of the category recursively until it finds the root category. I agree that the "category_path(#{cat.id})" part looks a little bit messy, but I couldn't find any cleaner way to do this with the eval solution I mentioned above. Any suggestions welcome.

The implementation

Now for the implementation. It's really quite trivial for the above defined set of the requirements. Just drop these two methods in your ApplicationController:

class ApplicationController < ActionController::Base
  ...
protected
  def add_breadcrumb name, url = ''
    @breadcrumbs ||= []
    url = eval(url) if url =~ /_path|_url|@/
    @breadcrumbs << [name, url]
  end

  def self.add_breadcrumb name, url, options = {}
    before_filter options do |controller|
      controller.send(:add_breadcrumb, name, url)
    end
  end
end

We need two versions of add_breadcrumb method. The first one (which is an instance method) is responsible for constructing the actual breadcrumb list. It is called several times during request processing. As we can see @breadcrumbs is just a list of two element lists, containing description and a URL.

The second version is a class method that we can use in declarations at the top of our controllers. It just defines an unnamed before_filter which, when called, uses the first version to add breadcrumb element. This way we get all the goodies (:only, :except, ordering) for free.

How to display the breadcrumbs? Put something like below to your layout and add HTML tags and styles as needed.

<% if @breadcrumbs %>
  You are here: 
  <% @breadcrumbs[0..-2].each do |txt, path| %>
    <%= link_to(h(txt), path) %> >
  <% end %>
  <%= h(@breadcrumbs.last.first) %>
<% end %>

And that's it. I hope it works for you. Any suggestions and improvements welcome.

UPDATE: I just posted a followup about how to make breadcrumbs work in an internationalized application.

UPDATE #2: you can have problems with using add_breadcrumb in your ApplicationController, as noted by Vinay in the comments below. See notes above how to fix this.

About these ads

38 responses to “Easy and flexible breadcrumbs for Rails

  • Dejan Simic

    I like it. Thanks. I would just suggest maybe a bit cleaner code for rendering:

    <%= @breadcrumbs.map { |txt, path| link_to_unless(path.blank?, h(txt), path) }.join(" > ") %>

  • Andrzej

    Clean and very elegant, thanks for sharing it!

  • craigtmackenzie

    That’s a really good solution, I’m going to update my post to link to this (i have loads of google references to my post…)

    Thanks for sharing!

  • CrazyDK

    In the implementation of ‘add_breadcrumb’ instance method, the parameter ‘url’ has to be eval-ed when it contains ‘_path’ or ‘_url’ string. Because, if ‘url’ was set to a string started with ‘/’, it is evaluated as RegExp and shows error.

  • szeryf

    CrazyDK, thanks for spotting this bug. I updated the add_breadcrumb method according to your suggestion. Or you could use root_path (it's probably defined in every Rails app anyway) instead of / :)

  • Dennis Bell

    Instead of testing for _url or _path suffixes, you could pass in a symbol, then use the code:

    url = eval(url.to_s) if url.is_a? Symbol

    that way you know the intent is to eval it instead of hoping the user doesn’t ever want to add a breadcrumb that has _path or _url as part of it’s url (which would be odd, but stranger things have happened)

  • szeryf

    Dennis, good tip, but that would make passing the _path/_url with parameter very awkward (like add_breadcrumb "...", :"thing_path(@thing)") so I think I'd rather stay with regexp checking of the params.

    There are so many ways to specify a URL in Rails app that I'm sure there's always a workaround if something is mistakenly evaled :)

  • Chris Bailey

    This is outstanding, thanks much! I’ve implemented it in the latest site I’m working on, and it’s working like a charm. The dynamic breadcrumb creation is particularly cool and useful.

  • szeryf

    Well it looks that zachinglis went and created a plugin based on (or maybe only inspired by) my findings. I haven’t tried it yet but it looks promising and has more features. Be sure to check it out.

  • szeryf

    And here’s another implementation of the above idea: http://github.com/fesplugas/simplified_breadcrumbs/tree/master.

  • Breadcrumbs II: the Internationalization « require ‘brain’

    [...] II: the Internationalization This is a quick followup to my breadcrumb solution. If your application is internationalized, I think you might be interested in the code presented [...]

  • owens

    WELL THANKS I AM A NEW USER I REALLY WANT TO KNOW WHAT
    IS ALL ABOUT BECAUSE AM INTERESTED

  • fornetti

    I do not believe this

  • vinay

    Hi,

    I am rails beginner. I followed same step you explained above. But
    it is giving “HTTP 500 error” and it is working fine when i remove (without home link in breadcrumb)
    ” add_breadcrumb ‘Home’, ‘/’ ” from application controller. Can i know what is wrong in my case.

  • szeryf

    vinay: it’s hard to say why do you get 500 error. Could you take a look into your log file and see if there’s any exception?

    I suspect that you use the method add_breadcrumb before you define it. If that’s the case make sure that “add_breadcrumb ‘Home’, ‘/’” line is under the definitions of those methods. I’m going to update the article to mention this.

  • vinay

    Thank you… now it is working fine.

  • Martijn Lafeber

    To keep your controllers skinny, the breadcrumb ideally is in a helper function. So, although this is a nice solution in many ways, I prefer a breadcrumb function in my application helper. Thanks for the insights though.

  • szeryf

    Martijn: I think you take the “skinny controllers” rule too far. In my understanding, the breadcrumb is essentially a function of where you are and where you come from. And that’s what controllers deal with, that’s their responsibility.

    Secondly, I don’t see how you could get the whole functionality proposed above using only helper functions.

    Another things is that controllers are more easily testable than helpers and this is important to me.

  • Josh

    Thanks, I just needed this functionality and this post did!

  • Eric

    Thanks for this good stuff! I added the eval business to the name param also just so I could throw the names of individual dynamic elements at the end of the crumb strings works great!

  • QAS

    for some reason i get two home crumbs and then the current crumb.

    ex: Home > Home > Model > 2003

    Any idea why this could be happening?

  • szeryf

    QAS: without looking at your code I can only speculate. You probably call add_breadcrumb 'Home', '/' twice. Please check your code for that.

  • Blake

    Thanks! Really useful!

  • Vinay

    this is sweet! thanks a lot!

  • fnl

    This is by far the cleanest and leanest implementation I have come accross so far (ok, eval is seen by many as “chaeting”, but… who cares, this is a relatively safe way of using it, especially if you make the regex sufficiently strict!). Kudos & thanx!

  • Saba

    First: Really good work. Thanks! But I have one wish :)
    Is there a clean way to remember a whole path?

    For Example:
    Computer -> CPUs -> IntelCentrino #1

    Where ‘Computer’ & ‘CPUs’ are categories and ‘IntelCentrino #1′ is a certain product out of a list.

  • szeryf

    Saba: it depends on what you need to store the breadcrumb for? The breadcrumb is just an array so there are many possibilities.

    If you want to store it for caching purposes, you can use :serialize in Rails to store it in model table. Or just throw the array at you local memcached.

    If you want to generate a page of links to various products and those links should look like breadcrumbs, I'm afraid you'll have to define a method breadcrumb_for in your model. But that's OK, because in this case those link are not breadcrumbs, only look similar :)

  • Saba

    Thanks for the answer szeryf. Actually I just dont want the breadcrumb-navigation to ‘reset’ if you change between different actions (could also be a change of models).

    It should be an advancement of the above mentioned dynamical way to recursively build the path for different categories.

    I already solved this problem for me by using sessions. Just wanted to know if there is a ‘cleaner’ way.

  • Nathan Duran

    This approach works pretty well for RESTful resources and all those other gimmicky goodies most people use Rails for, but it kinda falls apart for those one-off informational actions/pages that fall through to the default routes, never touch a database, and genuinely need to specify their titles/crumbs at some point after the controller’s called render.

    It also dies in what I believe would be an incredibly common practice: using controller_path as your URL. That method’s lack of a leading slash in its return value leaves you with redundant relative paths in your links, and there will likely be even more situations where simply punting to eval just isn’t going to work that I haven’t encountered yet.

    Much like user login management, this is one of those things there’s just no way to produce a generic, drop-in solution that’s flexible enough for everyone.

  • Jan

    This solution comes absolutely handy for a little project I’m on at the moment. Thank you! Still being a beginner @Rails, I wondered where to put my breadcrumbs method and had just started to type it into the application helper, when I decided to do a quick Google search. Glad I did :-)

  • Daryn

    what does the ||= do for @breadcrumb?

  • szeryf

    The ||= is conditional initialization in Ruby, i.e. it only assigns the value if @breadcrumb is nil.

  • Daryn

    so is @breadcrumb static (doesn’t erase after the first call to add_breadcrumb)?

  • Daryn

    I ask this because, I don’t want to have use use add_breadcrumb multiple times each time a go to a different page under a certain trail. I just want the path to be stores statically and added on to.

  • Fabio

    Hi, thanks for a nice breadcrumb solution. One suggestion: right now, the breadcrumb text is not html escaped, this opens up the risk of XSS. e.g. if @title contains “alert(‘hi’)” add_breadcrumb @title,foo will output the script verbatim instead of the safe “&ltscript…”

    The app can do the escaping, but since the breadcrumbs are set on the controller, it is a bit cumbersome:
    add_breadcrumb ERB::Util.html_escape(@title), …

    I suggest you consider escaping the output as part of the library instead of leaving it to the application.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: