Skip to content →

Easy PWAs the Rails way

Progressive Web Apps, or PWAs, have a home in Rails. There are a few pieces to add to any app to make it a PWA. With a little configuration, an app will be installable in a browser, accessible offline, and in line with any other front end framework.

PWAs?

Progressive Web Apps are a type of web app that can provide richer experiences online. Google has written up a guide on what they consider necessary for a delightful web experience here. Rails provides a lot of the necessary tools for page speed, such as Turbolinks and Russian Doll Caching. In order to be a PWA, an app needs to supply two specific files at specific paths: an app manifest, available at /manifest.json and a service worker, available at /service-worker.js. Here are two ways to provide these files currently in Rails: either in the public folder, or preferably, through Rails itself.

Providing the components

The first problem to consider is that the manifest file and the service worker file cannot be inside the asset pipeline path or the webpacker path. There is some discussion on Github about this, but the key part of the specification is that the service worker scope is dictated by the path of the file. If the service worker is served from /packs, then it can only be used for web pages that are in /packs. That’s not very useful. It’s therefore necessary to set up manifest.json and service-worker.js outside the asset pipeline or webpack.

You need to add some general configuration to your site. First, point to the manifest file in your application.html.erb head tag:

<!-- Lighthouse Details -->
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#C50001"/>

In your javascript, you need to register the service worker. This can go in application.js, either in webpack or in the asset pipeline location:

if (navigator.serviceWorker) {
  navigator.serviceWorker.register('/service-worker.js', { scope: './' })
    .then(function(reg) {
      console.log('[Companion]', 'Service worker registered!');
      console.log(reg);
    });
}

Then add the manifest.json and service-worker.js files as you need.

First Method: Public Folder

Write out the manifest.json file and the service-worker.js file by hand, and place them in the public folder in the Rails app. Files in the public folder path are served as is. One downside is that the asset paths are not available in the public folder. This means that those assets cannot be cached by the browser, and the app would lose some offline mode functionality.

Second Method: Service Worker Controller

By adding a controller that manages the /service-worker.js and /manifest.json requests, the asset paths are now available for all assets that need caching.

Make a controller called service_worker.rb and add two methods, like so:

class ServiceWorkerController < ApplicationController
  protect_from_forgery except: :service_worker

  def service_worker
  end

  def manifest
  end
end

The protect from forgery for the service_worker method allows the javascript to be served without any forgery request problems in rails.

In the views folder, make a folder called service_worker and add a manifest.json.erb file that looks something like this:

{
  "short_name": "My App",
  "name": "My Very Progressive Web App",
  "icons": [
    {
      "src": "<%= asset_path('icon_192.png') %>",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "<%= asset_path('icon_512.png') %>",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "<%= root_path %>",
  "background_color": "#fff",
  "display": "standalone",
  "scope": "<%= root_path %>",
  "theme_color": "#000"
}

Asset paths are available, with means that all the Rails asset pipeline or webpack features are available. Those icons referenced above also need to be provided.

Now, add a service-worker.js.erb file that looks something like this:

var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';

function onInstall(event) {
  console.log('[Serviceworker]', "Installing!", event);
  event.waitUntil(
    caches.open(CACHE_NAME).then(function prefill(cache) {
      return cache.addAll([
        '<%= asset_pack_path 'application.js' %>',
        '<%= asset_pack_path 'application.css' %>',
      ]);
    })
  );
}

function onActivate(event) {
  console.log('[Serviceworker]', "Activating!", event);
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
          return cacheName.indexOf(CACHE_VERSION) !== 0;
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
}

// Borrowed from https://github.com/TalAter/UpUp
function onFetch(event) {
  event.respondWith(
    // try to return untouched request from network first
    fetch(event.request).catch(function() {
      // if it fails, try to return request from the cache
      return caches.match(event.request).then(function(response) {
        if (response) {
          return response;
        }
        // if not found in cache, return default offline content for navigate requests
        if (event.request.mode === 'navigate' ||
          (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
          console.log('[Serviceworker]', "Fetching offline content", event);
          return caches.match('/offline.html');
        }
      })
    })
  );
}

self.addEventListener('install', onInstall);
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);

This service worker skeleton came from a Rails service worker gem that wasn’t working with webpack.

Whatever files need caching are added in the onInstall function. This template only has the webpack pack files, but any images, or asset pipeline files that should be cached can be added too.

Testing?

Google’s Lighthouse is the best way to test how an app matches the specification. Lighthouse will give recommendations, and list what’s missing. It can even test accessibility. Progressive Web Apps need HTTPS when not being served from localhost, so testing in Google Chrome during development is the easiest option.

Easy PWAs

PWAs with Rails are very easy to setup. They don’t necessarily get the webpacker/asset pipeline benefits of compilation and minimization, but the ability to serve the service worker and app manifest with just a little bit of configuration means there is no need to rewrite an existing app in order to make a web page sleek and modern.

Comments or Questions? Find me on twitter @jpbeatty

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

Published in ruby on rails

Comments are closed.