Among the three methods of caching views in Rails, I particularly prefer page caching. The main advantage of page caching is that it is delivered by the web server without accessing your Ruby on Rails server. In this blog post I'll describe how you can keep two separate caches: one for pc browsers and one for iPhone. You can easily extend it to support more than two versions.
Prerequisites
In this tutorial I assume you are using Apache to serve your application with mod_rewrite and .htaccess files enabled.
Final solution (for impatient readers):
-
Add the following code snippet in your ApplicationController
layout proc { |controller| controller.request.subdomains.first} before_filter :set_cache_directory protected: def set_cache_directory ActionController::Base.page_cache_directory = File.join(Rails.public_path, request.subdomains.first) end
-
Create two different layouts in your app/views/layouts directory. 'www.html.erb' for normal browsers and 'iphone.html.erb' for iPhone browsers.
-
Add the following rewrite rules to <RAILS_ROOT>/public/.htaccess. Create it if it doesn't exist.
RewriteEngine On # Force adding www at the beginning of URL # This rule is only applied when the host contains only one dot (.) RewriteCond %{HTTP_HOST} ^[^\.]+\.[^\.]+$ RewriteRule ^(.*)$ http://www.%{HTTP_HOST}%{REQUEST_URI} [R,L] # Redirect users to subdomains by user agent RewriteCond %{HTTP_HOST} ^www RewriteCond %{HTTP_USER_AGENT} Mobile.+Safari [NC] RewriteCond %{HTTP_HOST} ^[^\.]*\.(.*)$ RewriteRule ^(.*)$ http://iphone.%1%{REQUEST_URI} [R,L] #Caching for the index page (work around till I know a better solution) RewriteCond %{THE_REQUEST} ^GET RewriteCond %{REQUEST_URI} ^/$ RewriteCond %{HTTP_HOST} ^([^\.]*) RewriteCond %{DOCUMENT_ROOT}/%1/index.html -f RewriteRule ^(.*)$ %1/index.html [L] # Redirect to caches in different subdirectories # Each cache corresponds to different subdomain RewriteCond %{THE_REQUEST} ^GET RewriteCond %{HTTP_HOST} ^([^\.]*) RewriteCond %{DOCUMENT_ROOT}/%1%{REQUEST_URI}.html -f RewriteRule ^(.*)$ %1%{REQUEST_URI}.html [L]
Now if you access your application using a normal browser it'll use the layout 'www' and the cached files will be stored under 'public/www'. If you use an iPhone or iPod touch, it'll use 'iphone' layout and store cached files under 'public/iphone'. You can easily add more layouts by adding an entry to .htaccess file and adding a new layout file.
If you need to know how this works, read the rest of this blog post.
===================READ MORE===================
Starting
The first problem I faced is that POST, PUT and DELETE actions were served from cache which is not correct. You can refer to this blog post for a simple solution to this problem.
In this application I use two different themes: one for normal browsers (like FireFox) and one for iPhone browser (Mobile Safari). In my application I decided to render the iphone theme when the site is accessed using a specific subdomain (iphone.zilmenna.com)
This is accomplished by adding the following code to ApplicationController:
layout proc { |controller| request.subdomains.first}
This tell rails to use 'www.html.erb' layout file for requests coming to 'www.zilmenna.com', and 'iphone.html.erb' layout file for requests coming to 'iphone.zilmenna.com'.
When an action is rendered on a pc browser, the resulting page is cached. Future requests to this action will be delivered by Apache. What if these future requests were requested by iPhone? The same cached version (pc browser) will be delivered. This is unwanted behavior. A straight forward solution is moving to action caching which doesn't satisfy me. I needed to use page caching and freeing Ruby and Rails from executing any code. This is a hard problem but the solution is pretty simple. Follow the steps of this solution.
Use separate caches depending on the rendered layout
To make it easier to expand, I decided to use the subdomain as the folder name to store cache in. Just add the following code to ApplicationController:
before_filter :set_cache_directory
protected
def set_cache_directory
ActionController::Base.page_cache_directory = File.join(Rails.public_path, request.subdomains.first)
end
This saves views rendered to normal browsers under <RAILS_ROOT>/public/www. Views rendered to iPhone are saved under <RAILS_ROOT>/public/iphone. But, Apache still doesn't know about these subdirectories yet. It'll look under <RAILS_ROOT>/public which contains nothing. We can figure this out by doing some .htaccess tweaks.
Telling Apache where the cache is
I want Apache to look for cached files in a subdirectory named after the subdomain used to access the page. I had to revise the documentation to get it to success. Here's the code to add to .htaccess file.
# Redirect to caches in different subdirectories
# Each cache corresponds to different subdomain
RewriteCond %{THE_REQUEST} ^GET
RewriteCond %{HTTP_HOST} ^([^\.]*)
RewriteCond %{DOCUMENT_ROOT}/%1%{REQUEST_URI}.html -f
RewriteRule ^(.*)$ %1%{REQUEST_URI}.html [L]
RewriteCond %{THE_REQUEST} ^GET
In this line I check that the request is a GET request. I don't want to use caches for POST, PUT or DELETE requests, who wants to?
RewriteCond %{HTTP_HOST} ^([^\.]*)
In the second line, I match the subdomain used with this request. The matched subdomain is stored in %1 and will be used in the next lines. In line 3,
RewriteCond %{DOCUMENT_ROOT}/%1%{REQUEST_URI}.html -f
I check whether there's a file in the file system that corresponds to the requested path or not. Here the value %1 maps to the subdomain matched in line 2. If the file is found, I redirect the request to the matched file in the following line:
RewriteRule ^(.*)$ %1%{REQUEST_URI}.html [L]
This works fine for all requests except for one request: the home page. The home page is accessed using a REQUEST_URI of '/'. This doesn't map to a file in file system. If you are caching the homepage also you'll need to add the following set of rules.
#Caching for the index page
RewriteCond %{THE_REQUEST} ^GET
RewriteCond %{REQUEST_URI} ^/$
RewriteCond %{HTTP_HOST} ^([^\.]*)
RewriteCond %{DOCUMENT_ROOT}/%1/index.html -f
RewriteRule ^(.*)$ %1/index.html [L]
This is very similar to the previous one. I just added a condition to match with the homepage only.
Now, if the user is accessing the webpage using www.zilmenna.com, it'll search for files cached in /public/www. If the user is accessing it using iphone.zilmenna.com it'll find for cached files in /public/iphone.
But who'll tell iPhone users to go to iphone.zilmenna.com.
Redirecting users to different subdomains according to their browsers
This can be easily accomplished by adding the following set of rules in .htaccess file before the others.
# Redirect users to subdomains by user agent
RewriteCond %{HTTP_HOST} ^www
RewriteCond %{HTTP_USER_AGENT} Mobile.+Safari [NC]
RewriteCond %{HTTP_HOST} ^[^\.]*\.(.*)$
RewriteRule ^(.*)$ http://iphone.%1%{REQUEST_URI} [R,L]
In the first line I check that the site is accessed through www.zilmenna.com. If the user is accessing it through any other URL (iphone.zilmenna.com) I assume this is made on purpose and I don't check his browser type.
The last problem may occur when the user accessing the website using zilmenna.com, without any subdomains. This last piece is very easy. Just add the following lines to .htaccess
# Force adding www at the beginning of URL
# This rule is only applied when the host contains only one dot (.)
RewriteCond %{HTTP_HOST} ^[^\.]+\.[^\.]+$
RewriteRule ^(.*)$ http://www.%{HTTP_HOST}%{REQUEST_URI} [R,L]
The best part about this solution is that it's generic and very flexible. For example, I used this configuration with www.zilmenna.com; however, you don't see the word zilmenna in any part of this solution. This means you can safely copy and paste this configuration in your own site. Again, this solution only works when you are using Apache. You'll need to configure it on your development environment to be able to test it correctly.