Dynamic Named Routes for Semi-Static Pages in Rails

When I was designing the new UMSwing website, I had a few issues that, at the time, I didn’t have a clean method of implementing. One of those was the creation of semi-static pages. After watching this episode of Railscasts, I had a pretty good idea of how to implement them. The only issue with the solution offered was the lack of dynamically generated routes.

Semi-static pages are used everywhere on websites. They’re those pages like an “About” page, which has content on it that doesn’t really change that often. Typically, a controller would have to house these actions (/about, /faq, /contact, etc.), and  the routes specified manually. Railscasts came up with an ingenious idea to create a controller which was routed to /static/*, so that semi-static pages could be created on-the-fly and modified easily. It also allows for modifications to change without committing to a repository and going through the process of deploying all over again.

For those needing a quick Rails primer before going on, here’s the quick and dirty of what you need to know to understand this:

  • Rails is a MVC-based web application framework that runs on Ruby. In short, Ruby code is written to create webpages on-the-fly.
  • Every request in Rails is first put through the routes file in config/routes.rb. This file tells Rails which Controller and Action is run.

Okay, let’s get started. Let’s create our static pages scaffold (which includes model, views, and the controller). Obviously, there are sections of this that you would want to require authentication for (editing and deleting, for example), but that’s outside the scope of this tutorial.

script/generate scaffold pages title:string permalink:string content:text;
rake db:migrate

Now we need to modify our controller slightly. More specifically, our show action. Right now, it will respond to showing an element only when the ID is displayed. We want to modify it to handle a permalink as well (/about and /contact look better than /pages/135, don’t you think?). Here is your modified show action:

def show
  if params[:permalink]
    @page = Page.find_by_permalink(params[:permalink])
    @page = Page.find(params[:id])

Before we go any further, we need to create two custom methods in our model. These will format the permalink to remove any unwanted characters for the custom route name (replacing all unacceptable characters with an underscore) and for the URL (replacing all unacceptable characters with a forward slash to allow for nesting of pages). It’s also important to note here that previous validation should be done to ensure that the permalink does not have leading or tailing non-alphanumeric characters, but I removed that for simplicity’s sake.

class Page < ActiveRecord::Base
  def route_name
    p = self.permalink.gsub(/([^A-Za-z0-9])+/, '_').downcase # Change non-alphanumeric characters to an underscore

  def uri
    self.permalink.gsub(/([^A-Za-z0-9])+/, '/').downcase # Change non-alphanumeric characters to a forward slash

At this point, we can create and modify our pages as we would regularly expect from a new controller. All of our pages are accessible via /pages/1, /pages/2 etc. We now need to make our controller act as our catch-all (so that all requests that do not match any of the other controllers get routed to our Pages controller), and we also need to provide permalink support. Finally, we will dynamically generate customized, name routes for all of our semi-static pages. All of that gets accomplished with a few short lines of code. Add the following code to the top of your config/routes.rb file, starting at line 2 (inside the ActionController::Routing::Routes.draw section):

def map.static_page_actions
  pages = Page.find(:all)
  pages.each do |page|
    self.send("static_#{page.route_name}", "#{page.uri}", :controller => "Pages", :action => "show", :permalink => page.permalink)

Finally, we need to call this method close to the bottom of the code, right before our default catch-all routes.

map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'

What this method does is retrieve all of the static pages in the database, then creates a customized, named route for each page, telling Rails what each URI should look like, and where to direct the request to.

Hopefully this helps some people out with their dynamic page creation. I’m pretty sure there’s a pitfall or two here, but I think it could be taken care of by doing some simple route housecleaning in the Pages CRUD controller. The perk of this option is that it allows the routes to be named, and hopefully that is of some benefit for others.