On a recent project the team needed to do full page caching (ie generating a full HTML page) instead of relying on the standard Rails 4 caching options. The reasons were mostly to do with speed, as serving a static HTML page was much faster than any combination of action and fragment caches.
However the main issue we encountered with the common page caching gem was an inability to make this conditional. ie we wanted to cache the page only for non-logged in users. This short article is about how we solved this problem, which we split into 2 parts:
Displaying the HTML page if it is present
Generating the HTML page
To display the HTML page we simply inserted conditions into our controller, which then ran the alternate (full) path if conditions were not met. Assuming we had a ProductsController with a #show method, this became:
class ProductsController < ApplicationController
def show
if all_my_conditions_for_caching?
page_cached_html = File.join ([Rails.root, 'public', 'cache', 'page', "#{I18n.locale}_#{@product}.html"])
return render page_cached_html, layout: false if File.exists?(page_cached_html)
end
# ... the rest of the controller action without page caching
end
end
The method all_my_conditions_for_caching? is not shown, but simpy returns true or false for any conditions you specify whether to use page caching or not. eg if a user is logged in.
The page caching described above will only work if the HTML page is already existing on the filesystem.
We generate the HTML page by creating a simple class in lib/page_cache.rb:
class PageCache
# param :cache_folder, :string, desc: 'Folder to store this page'
# param :urls, :array, desc: "An array of url => desired page name hashes. eg [{'/products/my-cool-product' => 'en_product_1.html'}] "
def self.save_to_file(cache_folder = :page, urls = [])
app = ActionDispatch::Integration::Session.new(Rails.application)
urls.each do |route, output|
outpath = File.join ([Rails.root, 'public', 'gencache', cache_folder, output])
File.delete(outpath) unless not File.exists?(outpath)
resp = app.get(route)
if resp == 200
dir = File.dirname(outpath)
FileUtils.mkdir_p(dir) unless File.directory?(dir)
File.open(outpath, 'w') do |f|
f.write(app.response.body)
end
else
puts "Error generating #{output}!"
end
end
end
end
Some points to note here:
This method essentially makes a request to a URL and stores the output as an HTML file where specified.
You can use this class and method anywhere it is appropriate, such as in a rake task, cron job, or an after_save callback on another model.