Ruby on Rails gives you the ability to customize your page cache directory by specifying a config.action_controller.page_cache_directory in your environment configuration. For the purpose of this example, I’ll set mine to File.join(RAILS_ROOT, '/public/cache/'). It is in this directory that a new file will be created for each cached action request. Subsequently – only if your web server is configured properly – these static files will be served thereafter for each request. Page caching represents the best and fastest cache mechanism available in Rails since once the cache is created, Rails is no longer used at all.
There is however a catch with the page_cache_directory. It is a static class-level variable. That means, you cannot have it act dynamically based on any individual user or client request. So, if you’re building a multi-tenant application, where the content is unique to each subdomain requested, how do you properly separate (or segregate or silo) those cached files?
The answer I’ll propose here is to customize your Controller’s instance-level cache_page method:
class ApplicationController < ActionController::Base alias_method_chain :cache_page, :subdomain private ## # Inserts the currently requested subdomain as a directory in front of the # cached file location. Assuming you’ve moved the default page cache # storage location to /public/cache, a request to app1.example.com would be # cached as: # # RAILS_ROOT/public/cache/app1/index.html # (page_cache_directory/subdomain/request_path) # # If the request does not have a subdomain (a request directly to # http://example.com), the subdomain cache directory will default to ‘www’. # def cache_page_with_subdomain(content = nil, options = nil) subdomain = request.subdomains.last path = "/#{subdomain || ‘www’}/" path << case options when Hash url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) when String options else request.path end cache_page_without_subdomain(content, path) end end By chaining the method, we’re injecting the dynamic subdomain name into the beginning of the cached request path. The result is that you’ll have a public/cache/ directory with unique subdirectories for each of your subdomains.
The only outstanding bit, then is properly configuring your web server to scan the RAILS_ROOT/public/cache/ for a cache in the desired subdomain. Below, you’ll see an Apache mod_rewrite example. First, with no subdomain, Apache will default to ‘www’ (the same as what you saw in the application code), and second, it’ll match the subdomain name and use it when checking for a static file:
RewriteEngine On
RewriteCond %{HTTP_HOST} ^example.com$ [NC]
RewriteRule ^([^.]+)$ cache/www/$1.html [QSA]
RewriteCond %{HTTP_HOST} ([^.]+).example.com$ [NC]
RewriteRule ^([^.]+)$ cache/%1/$1.html [QSA]
You would need a similar mechanism with the expire_page method as well, so that it takes the subdomain into account which checking for static files to delete. But this should certainly a good start.

Maybe more simplistic but… don’t you think it would be simpler to do set
::ActionController::Base.page_cache_directory = RAILS_ROOT + “/public/cache/…/”
on application.rb based on subdomains ?
Thanks
The problem is that
ActionController::Base.page_cache_directoryis not dynamic. Once you set it at initialize, it won’t change thereafter. So, you can’t have it use a dynamic subdirectory based on subdomain name. For this project, that was useful since each subdomain has a different layout and page content.“Once you set it at initialize”
Indeed but I putted it in application.rb
I had to manage caching on multilingual website with some contents having the same url.
It’s set in a before_filter method and it worked fine for me.
Yes it is risky to put to change ActionController::Base.page_cache_directory for each request.
I have like this solution, but it did’nt work for me. It throws error. I have placed your code in application.rb
While starting the server it shows this error.
usr/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/core_ext/module/aliasing.rb:34:in `alias_method’: undefined method `cache_page_with_site_specific_change’ for class `ApplicationController’ (NameError)
Please help me fix this.
Thanks in advance ;)
::ActionController::Base.page_cache_directory = RAILS_ROOT + “/public/cache/…/”
don’t forget the “::” (both) at the beginning
Senthil: Feel free to comment with a link to a pastie or gist of your code, I can take a look at it. The code I pasted here isn’t code I use in production, but instead just a condensed recreation for easier illustration. So, it may very well not work via cut and paste. :)
plog: I’m certainly not saying this is a better solution than using a
before_filter, as I generally prefer not to monkey patch, so I certainly thank you for mentioning it.I think there are at least a couple of benefits from doing it the way I’ve mentioned vs. going through a filter that I should mention. Firstly, for actions which are not page cached, you’ll either needlessly process the directory assignment filter or you’ll be forced to skip the filter for each non-cached action, which would clutter the controller code. While neither of those options are terrible, I feel like both should be avoided if possible.
Secondly, the
page_cache_directoryis a class-level variable. So, if you’re simply resetting it per request, then if you find yourself in a threaded environment, you’re creating a race condition. It could one day happen that two requests come in so closely that one stomps the other and receives the incorrect content for a perfectly valid request. However, since there are currently no truly threaded environments for Rails this isn’t yet a major issue .. but work is certainly being done in that direction, and it will one day soon become one.Hi Nate,
Thanks for your comment.
alias_method_chain :cache_page, :site_specific_change private def cache_page_with_site_specific_change(content = nil, options = nil) return unless @site.get_property(“cache_page”) == “true” path = case options when Hash url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) when String options else request.path end path = “#{SITES_ROOT}/#{@site.short_name}/public/cache/#{@site.name}/#{path}” cache_page_without_subdomain(content, path) endI have slightly changed you function. But the requirement is same. Cache path has to change based on the request.
I have placed the following piece of code in directly in application.rb
Code is not formatted properly.
alias_method_chain :cache_page, :site_specific_change private def cache_page_with_site_specific_change(content = nil, options = nil) return unless @site.get_property(“cache_page”) == “true” path = case options when Hash url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) when String options else request.path end path = “#{SITES_ROOT}/#{@site.short_name}/public/cache/#{@site.name}/#{path}” cache_page_without_subdomain(content, path) endI was having the same problem as Senthil, getting the " undefined method `cache_page_with_site_specific_change’ for class `ApplicationController’ (NameError)" error.
Adding a before_filter to my ApplicationController may not be the ideal solution, but it’s working.
The RewriteRules were what really helped me out. Thanks!
Hey Travis.. glad some part of this helped you. I’ll certainly try to clear up some of the problems being mentioned when I come up for air on a few projects I’m currently working on.
I’ve also got rewrite rules available using this methodology for nginx if anyone might be interested .. let me know.
Thanks very much for this Nate – a neat solution. I’ve adapted it for multi-domains, and added in a fix: the method above overlooks the case where path is nil or /, and fails in Rails’ cache_page method as the generated path is a directory.
Also, the alias_method_chain should be after the method definition — some of your readers seem to be getting errors because of this.
</code>
Hey Andy, do you mind posting the rewrite rules for your revisions? Thanks!
Hi Nate,
great post, thank you. I’ve build a plugin around this adding some more functionality related to page caching. Feel free to check it out at http://github.com/yeah/page_cache_fu/tree/master
Thanks again,
Jan
I used this to create my cache pages for some static pages and my routes.rb looks like:
map.with_options :controller => “home” do |home| home.index ‘index’, :action => ‘index’ home.about ‘about’, :action => ‘about’ endAnd I get an error message when I try to load mydomain.com/about because the webserver looks for /cache/www/about.html and finds nothing. Rails then complains that no route matches /cache/www/about.html and returns a 404.
How can I have my index and about pages be cached and use my layouts? The pages are static, but I don’t want to create static html, I want to use the rails layouts and views, but have the pages cached.
Thanks!
any idea on how to handle parameters in url?