PWA is not a new concept but a lot of websites don't implement it yet.

I was recently invited to a Google Mobile Summit where Luis Dinís among others have shown me the light about PWA, AMP, and other mobile technologies.

It was so easy to implement PWA (at least the bare minimum) that I wanted to share it back to help other implements it too.

Create an offline page

First of all, its recommended to create an offline webpage. The Service Worker will redirect the visitors here if there are internet connection issues.

Ghost Offline Page

Create the Service Worker

I didn't have any Service Worker knowledge so take that into consideration.
Basically, my sw.js was glued together from different examples I found.

const cacheName = 'blogCache';
const offlineUrl = '/offline/';

/**
 * The event listener for the service worker installation and cache the offline page.
 */
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(cacheName)
            .then(cache => cache.addAll([
                offlineUrl
            ]))
    );
});

/**
 * Is the current request for an HTML page?
 * @param {Object} event 
 */
function isHtmlPage(event) {
    return event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html');
}

/**
 * Fetch and cache any results as we receive them.
 */
self.addEventListener('fetch', event => {

    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // Only return cache if it's not an HTML page
                if (response && !isHtmlPage(event)) {
                    return response;
                }

                return fetch(event.request).then(
                    function (response) {
                        // Dont cache if not a 200 response
                        if (!response || response.status !== 200) {
                            return response;
                        }

                        let responseToCache = response.clone();
                        caches.open(cacheName)
                            .then(function (cache) {
                                cache.put(event.request, responseToCache);
                            });

                        return response;
                    }
                ).catch(error => {
                    // Check if the user is offline first and is trying to navigate to a web page. If so serve offline page.
                    if (isHtmlPage(event)) {
                        return caches.match(offlineUrl);
                    }
                });
            })
    );
});

You also need a Manifest.json

Not all the fields are required and you can find more information about it on Google Manifest Page

{
  "name": "Tiago Rodrigues",
  "short_name": "TigPT",
  "description": "Dipping into programming one toe at a time.",
  "start_url": "/?utm_source=homescreen",
  "scope": "/",
  "display": "standalone",
  "background_color": "#000",
  "theme_color": "#000",
  "dir": "ltr",
  "lang": "en-US",
  "icons": [
    {
      "src": "./icons/icon-72x72.png",
      "type": "image/png",
      "sizes": "72x72"
    },
    {
      "src": "./icons/icon-96x96.png",
      "type": "image/png",
      "sizes": "96x96"
    },
    {
      "src": "./icons/icon-128x128.png",
      "type": "image/png",
      "sizes": "128x128"
    },
    {
      "src": "./icons/icon-144x144.png",
      "type": "image/png",
      "sizes": "144x144"
    },
    {
      "src": "./icons/icon-152x152.png",
      "type": "image/png",
      "sizes": "152x152"
    },
    {
      "src": "./icons/icon-192x192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "./icons/icon-384x384.png",
      "type": "image/png",
      "sizes": "384x384"
    },
    {
      "src": "./icons/icon-512x512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ]
}

Finaly inject the HTML code

I've added <link rel="manifest" href="/manifest.json"> from the Ghost dashboard.
Since I was there, I also added the theme-color <meta name="theme-color" content="#F1F6F9"> and the apple-touch-icon to complain with the PWA recomendations <link rel="apple-touch-icon" href="/icons/icon-192x192.png">

Since I use ajax.cloudflare.com and images.unsplash.com on almost every page, I'm leveraging a bit more speed by pre-connecting to these domains avoiding DNS resolve wait time.

I've added the serviceWorker registration to my Google Tag Manager that I could totally avoid but since I use my blog as a sandbox to test multiple services.

<script>
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}
</script>

Add the files to Ghost root folder

To add the files to your root folder in Ghost, you can download the installed Theme, add/modify the files and upload with a new name, you will have your own Theme that will not be auto-updated by Ghost new version.

Results

My website became supper speedy, serving almost every request from the cache and the load times dropped from about 2 seconds to 650 miliseconds with no cache and 130 miliseconds if the content is in the cache.

Google should also be happier with my website and give me a better score for organic traffic.

Lighthouse and Pagespeed results are much greener now and green is good! 😃

Next step, implement it everywhere!