I already said it multiple times, this blog was made as an experiment. I wanted to make my own server, host a site on it, optimize it and make it as secure as I can. The experiment turned out quite fun so far and the main site on that server is this simple Ghost blog.

Everyone around me knows that I'm a pretty big fan of the Ghost blogging platform and I'm honestly pretty happy with the feature set it comes with out of the box especially its default theme, Casper. Problem with Casper is that it's not PWA ready, but honestly its a pretty simple thing to fix.

First start with creating an offline page

You will need a page for when the connection is either unstable or outright not available so we will create that first.

  1. Go to your Ghost backend
  2. Go to your "page" section and create a "new page"
  3. Add whatever text you want and publish it

This is how mine is setup, I also have it to the /offline url. You don't need something terribly fancy, just something simple that works.

Create a manifest.json file

A PWA needs two things, a manifest.json file and a service worker active. I will be showing the most simple setup that I'm running here, you can modify it to your taste for your website.

  "name": "vidya",
  "short_name": "vidya",
  "start_url": "/",
  "theme_color": "#343F44",
  "background_color": "#343F44",
  "display": "standalone",
  "icons": [
      "src": "/vidyadev.png",
      "type": "image/png",
      "sizes": "192x192"
      "src": "/vidyadev512.png",
      "type": "image/png",
      "sizes": "512x512"

If you want more in depth information about the manifest.json file I suggest to go here. In our case, just modify the "name" and "short_name" field to your website name. You will also need to create three icons. Yes there's only two in the manifest.json, because the third one is for Apple devices. The third one though will be an exact copy of the 192x192 one which you will name "apple-touch-icon.png". It can be named something else but by default it will always catch that name.

Time for the service worker

The service worker is also pretty simple, we won't do one with super heavy caching and offline capabilities but it will still give you all the features you need.

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

 * The event listener for the service worker installation
self.addEventListener('install', event => {
            .then(cache => cache.addAll([

 * 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 => {
    const requestURL = new URL(event.request.url);

    // Ignore every request made from the admin panel
    if(/^\/ghost\//.test(requestURL.pathname)) {
        event.respondWith(async function() {
            return fetch(event.request);
    } else {
                .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();
                                .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);


You can just create a sw.js file and paste the entire code above inside it. You should modify the offline URL so it matches the path of your offline page.

Now I'll roughly explain how the code works. It adds a listener for every page fetch events on the domain of your website such as simply loading an URL. If the page isn't an admin page (so nothing that has /ghost/), don't cache or do anything to it. This is so your Ghost admin stays responsive and that all features work perfectly. After that part it simply checks if it returns a 200 response and if not it checks the cache in case the page is already there. If its not there, it loads the page and caches it afterwards.

Putting it all together

You will need to go to the "design" section of your Ghost admin and download your theme.

Once downloaded, unzip the file and go to the root folder of your theme.

It should look something like this. You will put your 3 icons, your manifest.json and your sw.js file inside the folder.

Now you can zip the folder again and upload it by clicking the "Upload a theme" button in the design section of your Ghost admin.

You will now go to the code injection section so we can load up the manifest and sw.js file properly.

In the Side Header box, you will add the following line at the first line:

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

Like so

In the Side Footer box, you will add the following lines at the end of the box:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {

This will simply verify if serviceWorker is available in the current browser and if so, it will register sw.js as a service worker.

And we are done

If you've followed each step correctly, you should see the following if you refresh your page in Chrome:

And the following in lighthouse:

And you should be able to add it to your homepage on Android or add to home screen on iOS via the share menu.

The current vidya.dev theme

You can find the current theme I use here: https://github.com/dSolid/Casper which is an up to date Casper fork with the added PWA functionality and a couple of tweaks to up the lighthouse score. The difference between my version and the tutorial here is that I also add push notifications using OneSignal, I will probably make a follow up post for that in a bit if people want to know.

That's all folks

If you have any questions, don't hesitate to ask in the comments. I hope I didn't miss anything and that my instructions were somewhat clear. It's my first time writing a piece like this and I will get better over time I hope.

Enjoy your PWA and as always, later gamers