Rails 4 Conditional Page Cache for Logged In Users

Feb 18th, 2015 in  by Michael Cho

A technique to generate a full static HTML page for caching, based on any specified conditions.

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:

  1. Displaying the HTML page if it is present
  2. Generating the HTML page

 

Displaying 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.

 

Generating the HTML page

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.

 


Other articles you may like

Understanding Time, DateTime, and timezones in Rails
Sep 13th, 2015
Rspec - How to test a controller has included concerns
May 18th, 2015
How to split Rails routes.rb into multiple smaller files
Apr 27th, 2015
Recommended Rspec style for Rails models
Feb 11th, 2015