Routes and Rails

When I designed this website, I wanted to keep a classic pages hierarchy like /projects/conceptoire.com/subsection/... .
Here is what went wrong, and what worked.

The way Rails is intended to work is actually pretty nice : every controller has its own url segment, which allows for a clean MVC and RESTful way to organize your web application. But this is the way it's mean to work ... not enough of a challenge for me, I had to do something ;-).
Actually the thing is I didn't want to browse through my website with urls looking like "http://www.conceptoire.com/blog_entries/34.html". I wanted a variable depth pages hierarchy and I wanted to map each of these pages to any action of any of my controllers. Beside that, this site was designed to be totally bilingual, and I found it would be good to have localized URLs to access ressources (like projects/conceptoire.com becoming projets/conceptoire.com in french).
Ok, I think you get it now, let's begin.
I considered two ways of achieving dynamic routing in Rails, and only one proved to be working. As usual I started to develop the whole thing using the wrong solution.

Solution 1 : did you say dynamic routes ? As in routes.rb ?

The first solution I implemented (I'll detail later why the second one didn't seem very attractive in the first place), is to dynamically generate my routes in the routes.rb file (ok calm down, I knew it was an odd idea, please drop the axe now).
This meant accessing a database to get the page hierarchy and map my routes to whatever action and controller specified for each node, and do that in every languages I support (which mean 2 ... not a big deal). And it seemed to work great ! With very little code I could detect a path written in english to automatically set the locale parameter. My controllers were mapped correctly so I could generate my localized paths back from an action ... it worked just as it should.
Now after a some time I commited the site on my production server ... and it worked. For a while.
Actually it worked until the server had to restart ... because it couldn't.
It took me three days to figure out what went wrong : the routes.rb is called at server initialization, but without loading the rest of the rails environment. It means no database, no helpers ... well, nothing that could allow my dynamic mapping to work.

Solution 2 : wildcards ? sweet !

The other solution was clearly the way to go from the beginning. For one thing : RadiantCMS was using it.
But it implied some more work to get all the neat features, like language recognition or url generation from a controller and an action.

Routing in ruby provides a world of possibilities, like checking that a url fragment matches a regular expression before routing ... or using a wildcard to capture any url.
So what I did is to map a route of the form "pages/*" to a dispatcher action (this will sound very familiar to RadiantCMS users). And here comes the troubles.
Because contrary to the RadiantCMS guys, I don't want to stick with this "catch-all" action, I want to dispatch the request to another action of possibly another controller, with some parameters, and all these informations are stored in the page objects in my database. But how to dispatch a call in Ruby on Rails when the route has already been chosen ?

First I try to retrieve the page object corresponding to the url the user is accessing (along with the language the url seems to be written in).
Then I merge the "params" array with the information from the page object (:controller, :action and other parameters), and I put it back into the original request.
Now comes the pure Ruby-esque part (inspired from the RoR source code) :

                  path = params[:page_path].to_s
                  @current_page,language = SiteNode.find_by_path(path)
                  set_locale(LOCALES[language.iso_639_1]) if not language.eql?(Locale.active.language)
                  params[:controller] = @current_page.controller
                  params[:action] = @current_page.action
                  params[:current_page] = @current_page
                  params.merge(@current_page.params)
                  request.path_parameters = params
                  # Redirect to the target controller
                  controller = "#{params[:controller].camelize}Controller".constantize
                  controller.process(request, response)
                  @performed_render = true
              
Yes, it actually get the controller class from a string. I just love this language.
Note the @performed_render=true to make sure RoR doesn't try to render the dispatch action, to avoid destroying the output from the target action.

 

I hope this could be useful to somebody. Just know that I learned a lot about RoR and it's routes by experimenting on this. If you have any questions, you know what to do !

Back