Acts As Static Content

Posted by ferrisoxide
on Thursday, February 26

Part 2 of the articles on content management in Rails. This was originally written almost a year ago, when I was slightly dumber than I am now. Included for completeness, but I really want to take this in a different direction. I should pull my finger out but for now I’ll maintain the illusion of industriousness by throwing up old blog posts.

Continuing the general rant aimless musing about Rails as a CMS

At work we use a single controller to serve up static content. The graphic designer on our team prefers to work with .html files in the file system so we use the static content controller to serve up web pages directly from disk. The files are ERB files – so ’.rhtml’ but close enough to be familiar to our designer. He also gets the bonus of all those helper methods, including custom helpers we build to make his life easier.

This approach has several advantages over keeping content in the database, like Radiant and others CMSs do. Firstly, publishing is just a matter of copying files into a known view directory. Secondly, we keep all content files under version control, so we can determine exactly which version of the content is to go live and which versions we can roll back to. We use branching to manage concurrent development of new content and maintenance of existing content.

We end up with the same model of development applying to both content and application development. The ancillary benefit is that everyone ends up talking the same language and content development ends up being managed with the same rigour that we apply to coding. This breaks down barriers in communication, allows us to work closely with designers and generally makes for a more enjoyable day’s work.

I want to use a similar approach with some documents I’m deploying as part of a web app. The documents aren’t going to change much but I don’t want to have to try to bundle them into the database using migrations. Apart from the duplication (the content would exist in both migration code and the database) I don’t want to have to build a new migration each time I make small changes to the text. Managing changes in a version control system is simpler so the static content controller approach suits here.

I also want to be to reuse this idea elsewhere, so I’m going to write the controller code as a plugin. If the code looks familiar, it’s because I nicked the original idea from here.

acts_as_static_content.rb

module ActsAsStaticContent

  def self.included(base)
    base.extend(ClassMethods)
  end 

  module ClassMethods
    def acts_as_static_content
      include ActsAsStaticContent::InstanceMethods 
    end  
  end  

  module InstanceMethods

    def static 
      #SMELL: assumes the convention app/views/foo for FooController
      if template_exists? path = "#{self.controller_name}/#{params[:id].to_s}" 
        render :template => path
      elsif template_exists? path += "#{self.controller_name}/index" 
        render :template => path
      else
        # pass through to the global error page handler
       method_missing params[:path].to_s
      end
    end

    protected 

      def each_page
        template_root = "#{RAILS_ROOT}/app/views/#{self.controller_name}" 
        excluded_actions = ['.', '..'].concat(self.hidden_actions)

        Dir.foreach(template_root) do |action|
          if !excluded_actions.include? action
            template_path = "#{template_root}/#{action}" 
            yield action, template_path
          end      
        end
      end  

  end
end    

ActionController::Base.send(:include, ActsAsStaticContent)    

The module code is introduced into controllers like any acts_as_X feature.

class MyController < ApplicationController

  acts_as_static_content

end

Any ERB files sitting in the /app/views/my folder will be available via the mixed in ‘static’ method. If we want this controller to serve up all static content by default we add this to the end of routes.rb:

map.connect '*id', :controller => 'my', :action=>'static'

Not much to it and as someone will ultimately point out you can serve up content like this with any controller methods – just drop any .rhtml file into a view folder and it behaves like an action anyway.

Putting aside any question of whether this sort of behaviour is actually desirable, the aim is to centralize the mechanism for managing content. The immediate value is in the ‘each_page’ method, as this can be used to add search features – not an explicit requirement of a CMS as per the previous post, but one my app will need and I suspect so will others.

I need to give the ‘static’ method a bit more smarts, but it’s a start. We’ve already ticked off one requirement – versioning – with a few more ready to be knocked down. This first cut will probably be unrecognizable after a few refactorings, but the only way to find out is to see what happens against a real app. So.. next post: eating my own dog food… through a straw.