Rails Static Pages

2011-04-18 13:00:00 +0000

Static pages are one of those things that you have to get around to doing for every website. This is how we implemented them on StillAlive to make them maintainable and also cacheable.

On pretty much every website, you will have a bunch of pages that are just plain old static content. Good examples of these are Privacy Policies, Terms of Use, Contact Pages and the like. These generally contain content that rarely changes.

To make these simple static pages, we first add a catch all route at the bottom of config/routes.rb which looks like this:

  match ':page_name' => 'site/pages#show'

As this is a catchall route, you need to put this right at the bottom of your routes file. Basically it just catches anything that is not defined in the above routes and send it off to PagesController#show with the params[:page_name] set to the requested path.

So if the user navigates to http://stillalive.com/plans_and_pricing then the PagesController will have the show method called with plans_and_pricing set as the params[:page_name]

With the routing set, we then make a Pages controller that looks like this:

class PagesController < ApplicationController

  layout 'site'

  def show
    @page_name = params[:page_name].to_s.gsub(/\W/,'')
  end

end

Some key points here, firstly, we set the @page_name variable to be the value of the params[:page_name], call #to_s on this (which handles the case of params[:page_name] being nil) and then call #gsub on the string stripping out anything that is not a word character.

With that done, we then drop through to rendering pages/show.html.erb which looks like this:

<%= cache "site-page-#{@page_name}-#{App.git_revision}" do %>
  <%= render :partial => @page_name %>
<% end %>

So this just renders the partial with the page name requested.

So in the example above, we have a partial called pages/_plans_and_pricing.html.erb that gets rendered.

We also wrap it in a cache block. The cache block includes the page name, and also our git revision. We set the git revision so that every time we push new code to our server, all the prior cached pages get expired. This provides a no brainer way to make sure the latest code is showing on the site.

The App.git_revision call is using the App Config gem and is set by modifying config/app.rb to look like this:

class App < Configurable # :nodoc:
  # Settings in config/app/* take precedence over those specified here.
  config.name = Rails.application.class.parent.name

  config.git_revision = `git rev-parse HEAD 2>/dev/null`.to_s.strip
end

This sets the value git_revision on boot. Simple.

Now the above code is good, but what happens when someone requests a page that does not exist on our site, we’ll, we can’t handle this the simple way, as the MissingTemplate exception gets raised outside of our controller, inside the show view.

One solution would be to wrap the render :partial call in the show.html.erb template in a rescue block, rescue the ActionController::MissingTemplate exception and render a missing template instead, however, the problem with this is that the response code will still be 200 OK, where we really want it to return a 404 page not found status.

So we need a way to check to see if the partial is valid or not, before we render the show action. There is no simple way to do this, so the way we handle it is by creating a partial_exists? method in the controller which checks, like so:

class PagesController < ApplicationController

  layout 'site'

  def show
    @page_name = params[:page_name].to_s.gsub(/\W/,'')
    unless partial_exists?(@page_name)
      render 'missing', :status => 404
    end
  end

  private

  def partial_exists?(partial)
    ValidPartials.include?(partial)
  end

  def self.find_partials
    Dir.glob(Rails.root.join('app', 'views', 'site', 'pages', '_*.erb')).map do |file|
      file = Pathname.new(file).basename.to_s
      # Strip leading _ and then everything from the first . to the end of the name
      file.sub(/^_/, '').sub(/\..+$/, '')
    end
  end

  # Do this once on boot
  ValidPartials = Site::PagesController.find_partials

end

By using a constant at the bottom, we only run the Dir.glob once at boot time to reduce the overhead (instead of running it on every request). The find_partials method returns a list of partials that look like:

['about', 'plans_and_pricing', 'privacy_policy'...]

This way, every time we restart our server, we get a new list of partials in production mode, and every time we refresh in development mode, this list gets updated in any case.

Hope this helps :)

blogLater

Mikel