Breadcrumbs in Rails

I’ve been working a little with Ruby on Rails recently.  One of the things I needed was a good module for handling navigational breadcrumbs.  I googled around a bit, but wasn’t able to find anything that fit my needs.  The closest to what I wanted was this example on stackoverflow.  It was a great starting point, but I ran into issues when I wanted to use nested controllers.  I wanted something a little more flexible.  Here’s my final solution.  I’m still a complete noob at rails, so feel free to point out any improvements you may have.

Features:

  • Splits up the URL and looks up the controllers for each section
  • Assumes you have a ‘name’ field on your data structure
  • Doesn’t link the last item since it should be the current pag
  • Works with nested controllers
  • Converts underscores to spaces and titleize’s the labels
  • Easy to modify to fit your own purposes

Code:

def get_bread_crumb(url)
    begin
        breadcrumb = ''
        so_far = '/'
        elements = url.split('/')
        for i in 1...elements.size
       
            so_far += elements[i] + '/'
           
            if elements[i] =~ /^\d+$/
                begin
                    breadcrumb += link_to_if(i != elements.size - 1, eval("#{elements[i - 1].singularize.camelize}.find(#{elements[i]}).name").gsub("_"," ").to_s, so_far)
                rescue
                    breadcrumb += elements[i]
                end
            else
                breadcrumb += link_to_if(i != elements.size - 1,elements[i].gsub("_"," ").titleize, so_far)
            end
           
            breadcrumb += " » " if i != elements.size - 1
        end
        breadcrumb
    rescue
        'Not available'
    end
end

Usage:

<%= get_bread_crumb(request.request_uri) %>

Which converts a URL like this:

/admin/posts/12/comments/8

to breadcrumbs like this:

Admin » Posts » Breadcrumbs in Rails » Comments » Great Post!

  • Ben

    You can change the code for is_int to use regular expressions like so:

    value =~ /^d+$/

    • You sure? That doesn't seem to be working for me. Is that version specific?

    • you're right though, a regular expression would be cleaner. i'm going to use this instead:

      elements[i] =~ /^[0-9]*$/

      thanks for the feedback!

      • Matt

        It should be:

        <code>

        elements[i] =~ /^d+$/ # d means digits, + ensures at least one

        </code>

        If you don't require at least one digit, an empty string will match (""), like when a URL like "foo//bar" is requested.

        • Eh, it's taking out the backslash in front of the d in the regex. Lame.

          • Yeah that's annoying. I'll bring it up to my friend at Intense Debate and see if we can get that fixed.

      • It should be:

        <code>
        elements[i] =~ /^d+$/ # d means digits, + ensures at least one
        </code>

        If you don't require at least one digit, an empty string will match (""), like when a URL like "foo//bar" is requested.

    • Ben's missing a before the d. You should also use + instead of * if you want there to be at least one digit.

      /^d+$/

      • Just realized the forward slash was getting removed from comments

        • ah, i figured there had to be something like. good catch.

  • Jason

    Nice! Here's a couple tips on making it more idiomatic. Ruby has lots of nice shortcuts and helpers:

    elements.each do |e| (vs the for loop, you'll rarely see for loops in Ruby)
    if elements[i].class == Fixnum (vs your is_int function)

    • Thanks! Duly noted.

      I'm getting an array back for element[i].class instead of Fixnum.

      • Be careful of your plurality:

        element[i] != elements[i]

    • Oh and the reason I went with the for loop was because I needed to be able to get back to the i-1 element. Is there an easy way to do that using your proposed syntax?

      • Brent

        you can get the index of element by using something like this: elements.each_with_index { |e,i| … }

      • each_with_index

    • Anon

      Don't do foo.class =, do foo.is_a? X

      In this case, X should be Integer rather than Fixnum.

  • Here's my rewrite: http://gist.github.com/445553

    Shouldn't need eval. And Array#join is your friend.

    • Sweet. That looks good, but it doesn't seem to be working quite right. The numbers still show up in the output instead of the name values.

      For example:

      Admin » Posts » 12 » Comments » 8

      • Josh, the rescue block is likely picking up the failed active record call. I'm guessing you're going to want to change that section to …elements[i-1].classify.constantize.find(element).name.humanize… (note the addition of classify, replacing singularize and camelize from your original code)

      • That probably means there's an exception every time… It might be because of "users".constantize should be "users".singularize.camelize.constantize

    • Great rewrite. No offense to Josh, but the eval and for loop were killing me.

      • Yeah as you can tell from my blog, I'm a PHP guy and am literally just getting started with this whole rails thing. I'm super grateful for all the tips here. It's cool to be a part of a community where I get this much great feedback on some code (on a Saturday night no less!)

        • Welcome to the community and apologies for the blunt remark about your code. I came to Ruby/RoR from PHP a few years ago and the first thing I realized was that everything I thought I knew how to do in Ruby was wrong. It was one of the best decision I've ever made though.

          • No offense taken. A big reason for me posting it was to try and get feedback on what I can do better.

          • This is a *great* way to get feedback, and clearly it's working. When I was starting out as a professional photographer, I posted lots of my horrible photos to mailing lists and sites designed around providing critical feedback from all kinds of levels of photographers. This taught me so much and improved my craft, my awareness, and my understanding of my own work.

            Keep it up!

  • I might refactor this somewhat to be more idiomatic and clear, such as http://gist.github.com/445588

  • Just to clarify, I changed the call to Record#name to be Record#display_name and then I would define a custom method on each of the models that would show up in the breadcrumb, allowing for lots of customization and defaults. For example:

    class User < ActiveRecord::Base
    def display_name
    "%s %s" % [self.first_name, self.last_initial]
    end
    end

    or even

    class Post < ActiveRecord::Base
    alias_method :display_name, :title
    end

  • Thinking about it, I was kinda disappointed that there wasn't an obvious way to iterate over an array with the previous and the next elements conveniently available. Seeing the Enumerable#each_cons method, I saw that it was most like what I wanted, so I decided to hack this together:
    http://gist.github.com/445605

    Wrapping this further there may be a way to define methods Enumerable#first? and Enumerable#last? to make it easier to add tests without requiring tracking the index, et al.

  • Nearly all that code is unnecessary if you are using resources in Rails. You don't even need to parse the URL (which is kind of gross, IMHO). Something like this should do everything: http://gist.github.com/445652

  • weppos

    Sometimes, you can't rely on the URL because it doesn't contain all the necessary information.
    For this reason (and to get more flexibility), I created http://github.com/weppos/breadcrumbs_on_rails.

    Feel free to try it and let me know your feedback 🙂

  • Worth reading "The problem with breadcrumb trails" before going all out http://derivadow.com/2010/02/18/the-problem-with-

    • Thanks for sharing. Interesting stuff.

    • Anu

      Good point!