Converting a Ghost blog to a Progressive Web App

Regular readers of this blog might notice a slightly different look and feel to this site. I’ve recently updated this blog and moved it from a custom ASP.NET website that has been running since about 2009 to Ghost CMS.

Ghost blog Progressive Web App

The old blog was working as expected, but an overall refresh was long overdue. I’ve also been itching to add a few new features including updating it to work as a Progressive Web App.

In this article, I am going to take you through the different steps required to get a Ghost blog up and running as a Progressive Web App (hint: it’s easier than you think!)

Creating an offline page

Apart from providing a slick user experience, my goal for the site was to harness the full power of service workers and bring powerful Progressive Web App features to this blog. I wanted the site to be super fast - once someone has read an article that won’t change, why not serve it from the cached version on their device.

I also wanted the site to work completely offline, so that if you are reading something interesting on the go, there is no reason why you have to wait for a network connection again. With this in mind, I set about creating a really basic offline fallback page in Ghost by logging into the admin screens and by clicking on New Story.

Ghost CMS Progressive Web App

I needed this Story to exist as a standalone Page, because ultimately this will become a fallback for when the user hasn’t visited a page on the site, but doesn’t have a network connection. I also didn’t want this offline page to feature in the list of new articles! In order to change this, I updated the post settings and checked Turn this post into a page.

Ghost CMS turn into page

When I visit the URL /offline, I can now see this page in action.

Creating the Service Worker

Once the offline page had been created, I needed to create a service worker file to start handling the offline and caching requirements for the blog. I started by creating a new service worker file and named it sw.js.

I then added the following code to the service worker file.


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

/**
 * The event listener for the service worker installation
 */
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 (isHtmlPage(event)) {
                        return caches.match(offlineUrl);
                    }
                });
            })
    );
});

Woah! The code above seems quite complicated, but let’s break it down piece by piece.

When the user visits the site for the first time, the service worker will begin downloading and installing itself. This is the perfect point to cache our offline page for later usage - this is also known as precache.

After adding an event listener for the install event, we can open the cache and begin adding resources into it. Next we call cache.addAll() and pass the URL for the offline page. Behind the scenes, the event.waitUntil() method uses a JavaScript Promise and uses it to know how long installation takes and whether it succeeded.

Now that we have our offline page cached, we need to start caching any other resources that the user might collect as they visit other pages on the site. By adding an event listener for the fetch event, we check if the incoming URL matches anything that might exist in our current cache using the caches.match() function. If it does, then we simply return that cached resource. However, if the resource doesn’t exist in cache, then we continue as normal and fetch the requested resource.

You may also notice that I'm not caching any HTML pages. The reason for this is that I always want to get just the HTML page from the network in case there are any changes. If for any reason the user is offline, it will fallback to the default offline page.

As a side note; Service workers only work on secure origins such as HTTPS. However, when you are developing Service Workers on your local machine, you are able to use http://localhost. Service Workers have been built this way in order to ensure safety when deployed to live, but also flexibility to make it easier for developers to work on their local machine.

If you are interested in creating powerful Progressive Web Apps that require more complexity than the code above, I highly recommend using Workbox.js. is a set of libraries and Node modules that make it easy to cache assets and take full advantage of features used to build Progressive Web Apps.

Add a manifest file

A web app manifest file is a simple JSON file that provides useful information about the application (such as its name, author, icon, and description) in a text file. But more specifically, the web app manifest enables a user to install web applications to the homescreen of their device and allows you as a developer to customise the splash screen, theme colors, and even the URL that's opened.

In order to get the full goodness of a Progressive Web App, I needed to create and reference a manifest file in my blog. My manifest file looks a little like the following code.

{
  "name": "Dean Hume's Blog",
  "short_name": "Dean Hume",
  "start_url": "/",
  "theme_color": "#000",
  "background_color": "#000",
  "display": "standalone"
}

For those of you with a keen eye, you may also noticed that I haven’t included every single property in my web app manifest file. The reason I did this was because I didn’t want to overload every user to this site with an install banner. If you are interested in the full capabilities of the web app manifest file, I recommend reading this article on the Google Developers website for more information.

We’ll be referencing both the manifest file and our service worker when we put everything together shortly.

Putting it all together

The great thing about using Ghost CMS is that you can quickly upload a new design using the admin panel. I used this functionality to upload the newly created service worker and manifest file. In order to do this, head over to the admin section of your Ghost CMS and navigate to Design and download the latest theme that is active on your site.

The site design will download as a compressed (ZIP) file which contains all of the assets that make up your site. Once you’ve extracted these assets, simply add the newly created service worker file (sw.js) and the manifest file to the root of the extracted folder. If you then compress (ZIP) this newly created folder with the new assets, you can upload a new copy by choosing Design > Upload a theme.

We are almost there! Now that all of the files have been uploaded we can start referencing them. Ghost has a great feature that allows you to inject code into either the header or footer of every page on your blog. This functionality was perfect for my needs.

Head over to Code Injection and add the following code into the blog header to reference the manifest file.

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

Next, I added the following code to the blog footer to reference the service worker file.

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

That’s it - both the service worker and web app manifest file have now been added to the site. We now have a working Progressive Web App!

Testing that it works

Before we get too excited, let’s test to see if our changes have taken effect. I find the best way to test a Progressive Web App is by using a built-in tools inside Google Chrome called Lighthouse.

Start by opening Google Chrome and navigating to the URL you want to test. Next, fire up the developer tools and head over to the Audits tab and perform an audit.

Lighthouse Progressive Web App

Lighthouse will generate a very useful report that will give you a detailed analysis of your website. If your Progressive Web App is running as expected, you should see a reasonable score (80+).

I hope that you found this article useful!