By Sean Reifschneider Date October 2, 2005Rails has the neat "caches_page()" method that will cache a rendered page and put it into your "public" directory. The benefit of this is that once the cached page is rendered, Apache will serve it as static content. This is an order of magnitude faster than having to pass off the request to Rails, even if Rails is using the cached page results. However, this is only true if the filename that Rails writes the cached page to matches up with incoming URLs that Apache sees. That's the problem. Worse, if you're using the PaginationHelper to make your long list paged, that will normally use URLs of the form "http://sitename.example.com/app/controller/action?page=1". Because of the "?page=1" GET parameter, Apache is looking for a page named after "action" in the "controller" directory, and doesn't care about the GET parameter. Without this, all of the pages end up showing the contents of the first page that's cached.
map.connect(':controller/:page', :action => 'index', :page => /\d+(\.html)?/ ) map.connect(':controller/:action/:page', :page => /\d+(\.html)?/ )Note that I made it so that the ":page" value can match just a number (like "2") or the number with a ".html" suffix ("2.html"). This is required for the Apache static page cache which I'll discuss next. I found that PaginationHelper doesn't mind if the page number has ".html" appended to it, so I didn't have to modify my controller at all. Also note that this kind of demonstrates a weakness of Rails Routing. It has a forward mapping (from URL into :controller/:action/:id, for example), but it doesn't have an explicit reverse mapping even though it's used in reverse. In some cases it's easy to do the reverse mapping, but in the above case there's no way of specifying that "url_for()" should create URLs of the form "controller/action/1.html". If you set a map of ":controller/:action/:page.html", it maps properly forward, but the reverse results in "controller/action/:page.html". In other words, the ":page.html" is literally left in the resulting URL. I don't know if this is just a bug, or is a weakness of the fact that Rails doesn't have a reverse map. Some way to optionally specify the reverse mapping would be useful. I love that the default action works without any extra effort, but sometimes you just need to "take the stick". After I made the Rails Routing change, I figured I'd then have to go into the Pagination helper and get it to make the pagination links which are generated stop using the "?page=" parameter. Having recently read the PaginationHelper documentation, I wasn't sure that this was going to be possible without hacking the code. As I was digging around, I realized that Rails had already worked all this out and no changes were necessary. Because of the "url_for()" helper, which generates URLs using the Rails Routing map, I didn't have to make any changes, it happened automatically. Deee-lightful. Now that we have URLs that are unique for every page without relying on the "?page=" parameter, we need to make it so that Apache can find these pages.
RewriteEngine On #RewriteLog /tmp/rewrite #RewriteLogLevel 2 RewriteRule ^/CONTROLLER/ACTION/(\d+)$ /CONTROLLER/ACTION/$1.html [L] RewriteRule ^/CONTROLLER/ACTION/$ /CONTROLLER/ACTION.html [L] RewriteRule ^/CONTROLLER/ACTION$ /CONTROLLER/ACTION.html [L] RewriteRule ^/CONTROLLER/(\d+)$ /CONTROLLER/$1.html [L] RewriteRule ^/CONTROLLER/$ /CONTROLLER.html [L] RewriteRule ^/CONTROLLER$ /CONTROLLER.html [L]The words in caps will need to be changed for your deployment. These are all internal RewriteRules (in other words, they do not cause a redirect to be sent to the browser), and the "[L]" means that if it matches no further rules will be tried. The first RewriteRule line in each group above are meant to handle the PaginationHelper changes we made above. So, a request for "http://.../controller/1" gets changed into a request for "http://.../controller/1.html". This means that Apache will find the page if a cached version has been written to your "public" directory. The second and third RewriteRule lines (in each group) are to handle requests for the base action or controller. You will need one for the controller and one to match each action. Or, you could get fancy and do:
RewriteEngine On #RewriteLog /tmp/rewrite #RewriteLogLevel 2 RewriteRule ^/([^/]+)/([^/]+)/(\d+)$ /$1/$2/$3.html [L] RewriteRule ^/([^/]+)/([^/]+)/$ /$1/$2.html [L] RewriteRule ^/([^/]+)/([^/]+)$ /$1/$2.html [L] RewriteRule ^/([^/]+)/(\d+)$ /$1/$2.html [L] RewriteRule ^/([^/]+)/$ /$1.html [L] RewriteRule ^/([^/]+)$ /$1.html [L]These are regex rules, the second set for just reaching the controller (via the index() method), and the first set is for the :controller/:action. Both of them have the rule (the first of each set) for the pagination mapping.