Distributing Twine stories for offline usage with Progressive Web Apps

A few days ago some of us were discussing how to distribute Twine stories for offline usage. For desktop/laptops we can use something like Electron, but not only is that more complicated than it needs to be - it’s also not an option for tablet/mobile usage. Twine stories are highly interactive, so we can’t easily port them to another format either. So we needed to publish html that continues to work offline, and ideally is cross-platform. Which sounded very much like the Progressive Web Apps or PWAs that all my frontend developers keep talking about, so lets how we can turn a Twine story into a PWA.

Following Google’s developer guide on converting to Progressive Web Apps and skipping over the bits we didn’t need, we discovered that making a Twine story available offline through the use of PWA technologies is actually really easy and that we should probably do this as standard.

If you’re interested in the details, then the guide linked to above will take you through everything you need to know including how to debug and prove it is working as expected. If you’re just interested in getting something working quickly then here’s the minimum you need to do:

  1. Add a manifest.json file which describes your Twine, including a name and logo amongst other things.
  2. Add some custom javascript which loads a Service Worker that does all the caching work.
  3. Ensure you are serving the Twine story over https - it won’t work over plain http (except for local development using localhost)

Manifest.json

This requires editing the html code of the Twine story, which means you will need to do this every time you publish a new version. However, it is just one line of code in the html file that you will need to remember to add - the manifest.json file itself won’t need touching after it has been created.

Add the following line to your html file anywhere between the two head tags.

<link rel="manifest" href="manifest.json" />

Then create a manifest.json file which describes your Twine:

{
  "name": "Flights2Twine",
  "short_name": "🛫🛬",
  "display": "mininal-ui",
  "start_url": "/",
  "theme_color": "#7FDBFF",
  "background_color": "#7FDBFF",
  "icons": [
  {
    "src": "icon-192.png",
    "sizes": "192x192",
    "type": "image/png"
  }
]
}

It is important that the json parses correctly and that the image actually exists, otherwise you will get unexpected/unhelpful errors.

With the manifest.json file in place and linked to your users can now add the page to their homescreen and get a nice icon.

Service Worker

The Service Worker is what actually does the caching. There are two components here, some javascript to load the Service Worker and the Service Worker itself.

Again, we need to edit the html of the Twine file and add some additional code. We have opted to add this extra javascript just before the closing </body> tag so that it does not block the loading of the page.

<script>
navigator.serviceWorker && navigator.serviceWorker.register('./worker.js').then(function(registration) {
  console.log('Excellent, registered with scope: ', registration.scope);
});
</script>

This javascript code refers to the file worker.js which we now need to create:

// version: 0.1.0
self.addEventListener('fetch', function(e) {
    e.waitUntil(
        caches.open('the-magic-cache').then(function(cache) {
            return cache.addAll([
                '/',
                '/index.html',
                '/manifest.json',
                '/icon-192.png'
            ]);
        })
    );
});

self.addEventListener('fetch', function(e) {
    e.respondWith(
        caches.match(e.request).then(function(response) {
            return response || fetch(e.request);
        })
    );
});

There are two functions here - the first does the caching, and the second intercepts requests to load the assets from the cache if available. The example we are working on is a very simple Twine story with just the html file. If you are adding any other images, sounds, javascript files, etc. then you will need to add each of these to the first function otherwise they will not be cached.

It’s also important to note the version number in this worker.js file. This is extremely important, because the way you invalidate caches in this most simplistic invokation of Service Workers is by changing the contents of the Service Worker file.

HTTPS

We’ve left this until last, because this is in relation to how you serve the resulting files. However, it’s actually the most important part. If you are not serving the content over HTTPS then you cannot use any of this, at least not in Google Chrome.

Final thoughts

This is a very quick and easy way of getting offline support for your Twine story across multiple platforms. After the first load your users should be able to use your Twine story as much as they like, even when they are offline. Aside from the annoying steps to edit your html before publishing, there should be no reason not to do this for every Twine story you generate.

If you are loading external resources, ie. ones from another server, then you will need to handle these differently. But for the majority of Twine stories this simple implementation should work nicely.