We recently deployed an offline enabled page in an existing Rails 3.0 application. We started understanding how to implement offline functionality be watching the railscast on offline apps part 1. In this blog post we’d like to share what we had to learn beyond what Ryan Bates outlined in that screencast, especially when it came time to deploy the application to heroku.com.
Create and serve the cache manifest file
Rack-offline makes it extremely easy to dynamically generate and serve the cache manifest file.
Add this to your gemfile:
Add this to your routes.rb:
match "/application.manifest" => Rails::Offline
Rack-offline generates the manifest file based on the contents of the public directory (Rails 3.0) or the assets directory (Rails 3.1 and beyond). It sets the content type to text/cache-manifest which is required by the browser, and it creates an endpoint to add to your routes.rb file so that rails will serve that file.
Add the manifest attribute to the html tag
To make your page offline capable you need to add the manifest attribute to the html tag. That manifest attribute must point to a location on the web server that will serve the manifest file.
This is where things start to get tricky. Once the browser detects the manifest attribute and successfully downloads and caches the contents of that manifest your web page will always load from the application cache. ALWAYS. This really tripped me up. For some reason I assumed the browser would only load the page from the application cache if the browser was offline. That is NOT the case. Whether online or offline, the browser loads the page from the cache.
The rack-offline readme describes the sequence of events as follows. (I’ve added some notes.)
- Immediately serve the HTML file and it’s assets from the Application Cache even if online
- Asynchronously download the file specified in the manifest attribute
- Compare the file byte-for-byte with stored manifest
- If the stored manifest and newly downloaded manifest are the same do nothing, fire event noupdate
- If the stored manifest and newly downloaded manifest are different download all assets specified in manifest, fire event updateready
At this point if the user refreshes the page they will get the newly updated cached assets. Or, you can call applicationCache.swapCache() to programmatically swap the new assets in.
Strategies for changing the cache manifest file to trigger step 5
Most blog posts suggest using a comment in the cache manifest to trigger a change. For example, a revision number.
# revision 42
If you change the revision number every time an underlying asset changes the browser will know to re-download all the resources listed in the manifest.
Rack-offline manages this for you by generating a SHA hash in a commented out line at the top of the manifest. It utilizes two strategies: one for development mode and another for production mode. In development mode the SHA hash is based on the timestamp for each request to the cache manifest, unless the second request (step 2 above) is within 10 seconds of the first, at which point rack-offline doesn’t change the comment. That 10 second interval is configurable with the
To change the cache_interval in rack-offline we added this to our routes.rb file
In production mode rack-offline generates a SHA hash based on the underlying contents of each file referenced in the cache manifest. Thus, if an underlying asset changes, the SHA hash will change. Rack-offline uses the
config.cache_classes to determine whether to use the development or production mode strategy.
config.cache_classes = false # rack-offline will use development strategy
config.cache_classes = true # rack-offline will use production strategy
You can change config.cache_classes for your development environment in /config/environments/development.rb,
Dealing with the cache busting timestamp
Rails 3.0 appends a cache busting timestamp to each asset referenced in your layout like application.html.erb.
It turns this
This is very cool.
However, this timestamp query string and rack-offline don’t play well together “out of the box”. The timestamp basically breaks the ability to reference the cached assets because rack-offline doesn’t add the timestamp query string to the cache manifest file. The browser thinks the files referenced in the head are different from the files cached in the application cache. That is why Ryan Bates recommends you set
ENV["RAILS_ASSET_ID"] = ""
This is a great quick fix to get going, but it wreaked absolute havoc on my application once I pushed to our staging environment. Why, because I had no way of busting the browser caching. This meant changes to my assets never made it to staging because the browser was caching the files and I had no consistent way of busting that cache.
To solve this problem I had to get the cache manifest and rails asset name to match exactly. To accomplish this I monkey patched AssetTagHelper and configured rack-offline to have the same exact query string on the asset. This allowed me to gain back my cache busting ability and have the cache manifest work.
To configure rack-offline to add a custom query string cache buster to end of file names in cache manifest:
To monkey patch AssetTagHelper to set that same custom query string cache buster to end of file names in head:
I had to define my own cache busting timestamp because I wasn’t able to hook into the rails_asset_id from within rack-offline.
I recently watched the peepcode screencast on local storage and he shows us how to roll your own dynamic cache manifest solution. I highly recommend you watch that screencast. It’s based on a Rails 3.1 or higher application, so it would need to be modified for a Rails 3.0 application which is one of the reasons why I didn’t go ahead and implement it.
Dealing with stale csrf_meta_tags
Getting a Rails page served from the cache means the cross-site request forgery meta tags could be out of date. Rails provides a nice helper csrf_meta_tags which creates the csrf-param and csrf-token meta tags based on the current session. Unfortunately, as soon as you add the manifest attribute to a html tag you will no longer be served the page from Rails. You are served the cached page. This means you your csrf meta tags could be out of date and any post you do from that page will be rejected. This is what we did to deal with that. It makes me feel uncomfortable, but I couldn’t come up with a better solution.
Dealing with authentication
We use devise for our authentication. We found during our online/offline intermittent testing that sometimes we would lose the session. To deal with this we stored the current user in the local storage and called the method login_from_cached_page when the user went back online before doing anything else.
skip_before_filter :authenticate_user!, :only => :login_from_cached_page
We skip devise authenticate_user! method and then check to see if there is a current_user (devise method). If there is not, then we set the current user to the last user stored in local storage and sign them in.
Communicating the application cache status to the end user
We added event listeners on window.applicationCache to communicate with the user the state of the application cache.
Deploying on heroku.com
When we deployed to heroku.com, the updateready event fired every time we loaded the page. This was strange because we knew for a certainty that the underlying assets in the cache manifest had not changed. This fork of rack-offline solved that problem.
The two railscasts focusing on offline Rails applications. Really helpful.
Dive deep into how the cache manifest and the flow of events.
The rack-offline gem.
The peepcode on html5 browser caching. Also how to roll your own cache manifest generator.
A description of the application cache events.
- If your application doesn’t see any assets not reference in the cache manifest even though you are online, it might be because you left out the all import network section in the cache manifest.
- To remove you application cache go to chrome://appcache-internals/ and click remove.
- You can analyze the application.manifest by browsing to it. In your dev environment go to http://localhost:3000/application.manifest.