An Action-Packed Adventure
Posted at
Most of this past Sunday was spent finishing up a first workable stab at solving a small problem in Web development that I'd been thinking about for a long time. I don't ask for much: just a simple little template system that works well for a set of HTML documents which are mostly static, as opposed to a template system that's only really appropriate for writing highly dynamic Web applications. You know, just a little something that makes it easy to factor out the common bits of information that should be the same for all pages in a Web site so you can edit the common stuff in just one place and have the change propogate to all pages on the site.
This isn't a new problem by any stretch of the imagination, but all the solutions I'd ever seen (from HTML pre-processors to "include" directives in SSI or PHP) turned the pattern I wanted to use inside out. Instead of studding every single page in a big collection of HTML documents with commands to suck in a common header and footer, I wanted to keep my documents clean and free of their surrounding context as much as possible. The more generic container should include the specific document content, not the other way around.
Since I finally got around to treating myself to a serious education in Rails last week, I discovered that my prayers for a more beautiful alternative to the inclusion pattern were answered by its layout system. My only problem was that Rails is very heavily geared toward writing database-backed Web applications that follow the MVC pattern, and doing anything else with it goes against the many assumptions that are part and parcel of the framework. However, Ruby on Rails scores big points in my book by being very flexible about its assumptions. If you are patient enough and clever enough to figure out how, you can very cleanly override Rails' assumptions and I was thus able to scratch my Web development itch with a quite elegant solution.
Since I want to keep the bulk of the content for the Web sites I maintain in a directory-structured filesystem instead of a database, I was already violating Rails' most obvious assumption of database-centered storage. But it turns out that the more significant issue was that Rails assumes that each URL that it receives should correspond to a method in an instance of a controller class, while I wanted to retain the more boring, traditional custom of mapping from URLs into files in the filesystem tree. I certainly wasn't going to try to predict and enumerate the name of every file that I'd want to serve and hard-code a method for it in the URL-handling class for Rails. So I got around this at first with Rails' routing facility, specifically by using a PathComponent in a route that would direct everything to a single method in my document-serving controller class with the filename part of the URL passed handily as a parameter.
Very neat and clean: the only code I had to write was a very simple method that does a little preprocessing on the requested file name and calls a function to render the appropriate document with the right layout. But there's still a problem. When I actually deployed the site as a CGI program run by Apache HTTPD, it was horribly slow. The computer I use for my Web server might not have the most modern hardware in existence, but I still don't consider a 1.2 GHz CPU with 512 MB of RAM underpowered at all. And yet, the overhead for starting the Rails framework code is so large that it's completely impractical to wait for it for every single HTTP request. This is obviously why the Rails community is so big on FastCGI. So I tried using FastCGI. I really honestly tried. But neither mod_fastcgi nor mod_fcgid for Apache 1.x or Apache 2.x worked reliably for me. I spent enough time trying to debug the setup with completely insufficient error messages to get fed up with the affair.
It was easy for me to give up because I knew I had an alternative. I knew that I was only really using a small bit of what Rails provides, namely ActionPack, although that piece does happen to be one of the most significant ones. Since ActionPack can be used easily all by itself, I could use it to write a simple, fast, standalone CGI script that is nearly identical to the controller I wrote for use within the context of Rails. The main adaptation that had to be made was to compensate for the loss of the above-mentioned routing system. Without Rails' routing, I wasn't sure how to map all URL requests to the a single method within my controller class. I was back to working around the assumption that every different URL would be handled by a different method in the controller class. I knew there had to be a way to write a Ruby class that would respond to any method at all, without having to mention the name of the method in my code. I was goaded by the fact that I knew exactly how to do this simplistic sort of metaprogramming in Python, as I'd used this sort of trick in the past: just override the __getattr__ method, and the name of the attribute/method that the caller tried to invoke will be found as the first parameter. After doing enough looking around, I discovered that Ruby's equivalent is the "method_missing" method.
Voila! A CGI script that neatly renders any old HTML document that you've templated with embedded Ruby code, assuming that the name of the document is given in the "action" parameter passed either as a GET or POST variable. But we don't want URLs that look like "http://example.com/cgi-bin/document_controller.cgi?action=some/thing.html". We want "http://example.com/some/thing.html". So Apache's mod_rewrite comes to the rescue with the following .htaccess incantations:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /cgi-bin/document_controller.cgi?action=$1 [L]
And we're done. Documents that share a common layout without being riddled with superfluous repetition. Pretty URLs. All done with mere teeny snippets of code. I'm happy.
(Well, OK. The script certainly has room for improvement. Most significantly, different sets of documents on the same site should be able to use different layouts. Ideally, the decision of which layout to use would be based on which directory the document resides in. But that's something for another day.)
Comments
Comment from Daniel at
Comment from Abba at
Comment from Daniel at
Comment from Tim at