It is pretty easy to create a website, that can be installed on any mobile or desktop device with a modern browser. Learn how to transform your website into a Progressive Web App, that even works offline.

What is a Progressive Web App?

The term Progressive Web App (PWA) is mostly used for a website that supports one or more of following functionalities:

  1. Installable: The website can be installed on any phone or desktop computer with a modern browser similar to native apps
  2. Offline: Parts or even the whole website / app works without internet connection
  3. Push Notifications: The website is able to send push notifications to users without having to have the website open

I’m going to adress the first two points in this blog post.

Make a website installable

To make a website installable to a device, we need two things:

  • a manifest file
  • a service worker

The manifest file

The manifest.json file is used to give some meta information such as name, description and icons as well as if the app is going to be displayed as a standalone app or like a traditional website.

Creating a manifest file is pretty straightforward.

First we create a manifest.json like this:

{
    "name": "YOUR_APPS_NAME",
    "short_name": "SHORT_NAME",
    "description": "A_DESCRIPTION",
    "icons": [
        {
            "src": "icons/icon-32.png",
            "sizes": "32x32",
            "type": "image/png"
        },
        // ...
        {
            "src": "icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "start_url": "/index.html",
    "display": "standalone",
    "theme_color": "#ffffff",
    "background_color": "#ffffff"
}

And link it in out html head:

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

Note

Remember to add icons. Otherwise your app is not recognized as a PWA!

Find out more about the manifest.json file here: https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs

With Jekyll, which I use to generate this blog, you can even use Liquid to generate your manifest like so:

---
layout: null
---

{
"name": {{site.title}},
"short_name": {{site.title}},
"description": {{site.description}},
[...]

The Service Worker

The Service Worker handles installing apps, caching, notifications and a few more things.

TL:DR

Find the complete service worker file from this example here.

First we create a service worker file like sw.js and register it in out main Javascript:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js');
  });
}

In our Service Worker file we can listen for the install event, when the user installs our app:

self.addEventListener('install', function(event) {
  // Perform install steps
});

Caching for offline use

To cache our files to be accessible offline, we need to tell our service worker, which files to cache. Like so:

const cacheName = 'my-site-cache-v1';
const filesToCache = ['/', '/styles/main.css', '/script/main.js'];

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(filesToCache);
    })
  );
});


Because I use Jekyll, I can use Liquid to generate a cache name with a date, to make sure the cache is recreated whenever I update my page.

---
layout: null
---
const cacheName = 'sw-{{ site.time | date: '%s'}}';
[...]


Now we can return our cached files, whenever the user request them and fallback to fetching them when no cache is available:

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

Adding a offline fallback page

If you want to present you visitors an offline notice page, that will be shown, when the page is offline, we can catch errors when fetching:

self.addEventListener('fetch', function(event) {
    // [...]
      return fetch(event.request).catch(function(error) {
        return caches.match(offline_page).then(function(response) {
          return response;
        });
      });
    })
  );
});

We just need to make sure, the fallback page is always cached:

const offline_page = '/offline.html';
const filesToCache = [
  // all your files...
  offline_page
];

So when a user now tries to access a page that is not cached before when being offline, the fallback page is loaded from cache.

Deleting old cache

(See: https://timobechtel.com/111-challenge/pwa-caching/)

When updating the cache name (e.g. const cacheName = 'my-site-cache-v2';) the old cache is still there and will be used. To delete old caches we write a function for the activate event, like so:

self.addEventListener('activate', function(event) {
  event.waitUntil(
    // delete old caches
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(foundCacheName) {
          if (foundCacheName !== cacheName) {
            return caches.delete(foundCacheName);
          }
        })
      );
    })
  );
});

Find out more about service workers here: https://developers.google.com/web/fundamentals/primers/service-workers/

That’s it. Now you have a PWA that you visitors can now install on desktop or mobile!