How to build a PWA

PWAs are kind of new, but I don’t think they are going away any time soon. If you’ve done standard web development, it won’t be too difficult to learn how to build a PWA, and it will flesh out your list of marketable skills. If you don’t know what a PWA is, or why they are used, check out my blog post which explains this. For the purposes of this tutorial, just keep in mind that they have to be functional when offline, and they must be installable to both desktops and mobile devices.

In this tutorial, I’m going to make a very simple PWA, so you get a taste for how they work. I built a PWA from scratch, and wrote down the steps while I did it. If you follow along, you’ll learn about a few little “gotchas” that I encountered while building it, and how to avoid them, so your own process will be smoother.

This tutorial is up-to-date as of July 2019. It has been tested in Chrome 75.0.3770.100.

First, Build the Web Page

My PWA plays a short video clip from “Ain’t Nature Grand!“, a cartoon which is out of copyright now. I created my “video” from a series of image files, like a flip book, rather than just playing an actual video file, which would be less challenging (I think).

My laptop OS is Ubuntu 18.04. I opened the “Ain’t Nature Grand!” video in YouTube, recorded a short clip from it with recordMyDesktop, and then saved the recording to my home directory as ‘out-1.ogv’.

Next, I used ffmpeg to spit out the frames from my recording, like this: ffmpeg -i out-1.ogv thumb%04d.jpg -hide_banner (via bugcodemaster).

The result was 352 jpg files, from thumb0001.jpg to thumb0352.jpg. I copied them into a directory called media, and wrote some JavaScript code to display them one after another in a web page.

At this point, I had a “mini” web application, which you can view here.

It has very limited functionality. It can run on my laptop. But it does require Internet connectivity, and the images are not cached. It’s not a PWA.

So far, everything was pretty easy to do. The development that I did up until this point was done by editing pwa.html, and loading it as a file into my browser – it didn’t even require a web server to run the file. I only uploaded my files to my server when the page was clearly working well.

Turn the Web Page into a PWA Which Works Offline

In order to make this app work when my mobile device or laptop is offline, I added a ServiceWorker. ServiceWorkers require a “secure connection” (https, not http). My website can be accessed over both https and http (as of July 2019). You will have to visit the site via https in order for the PWA demos to work.

At first, I followed the basic instructions for using a ServiceWorker at Mozilla.

Here’s the new version of my PWA.

You can visit that page, right-click it with your mouse, and then “view source” to see what it looks like. Here’s the JavaScript file for the ServiceWorker. If you take a look there, you’ll see that the only thing my ServiceWorker does is cache the images and application files that make up my animation. So adding this ServiceWorker is just a first step in turning my app into a PWA. It’s not a really a PWA yet; it’s not installable, and it won’t run offline.

I ran into some problems when I copied these files over to my server!

  1. At first, I loaded the page via http. The JavaScript condition ('serviceWorker' in navigator) returns false in that case, so the ServiceWorker code goes unused, and nothing caches. When I did this, I saw the error “Site cannot be installed: Page is not served from a secure origin” in my DevTools Console (Ctrl-Shift-J). However, the page loaded fine, and the animation ran. It was just the ServiceWorker which wasn’t functioning. You really have to keep an eye on your Console when developing a PWA, otherwise you can miss things like this!

  2. When I switched the URL to use https, I saw several errors in the JavaScript console. One was:

A bad HTTP response code (404) was received when fetching the script.

The animation ran, but the ServiceWorker still failed.

This error happened because I’d passed the wrong argument to navigator.serviceWorker.register. I had passed in “/fullStackOasis-PWA-service-worker.js” (note the leading slash), like this:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/fullStackOasis-PWA-service-worker.js').then(function() {

But that only works if my demo is running from the web root. Since it’s running from the directory “pwa-1”, I should have set the argument to “/pwa-1/fullStackOasis-PWA-service-worker.js”. Once I did that, the error went away.

Caveat: When developing a PWA, you have to be really careful about your directory structure. Examples on the web often use the web root of a site for the PWA. If you are using a subdirectory, there are several places where you have to be careful about referencing files under your web root. I’ll talk about this more, below.

  1. After fixing the 404 error, I reloaded the page, and saw another error in the Console:
Navigated to
fullStackOasis-PWA-service-worker.js:30 [Service Worker] Install
pwa.html:10 CLIENT: service worker registration complete.
fullStackOasis-PWA-service-worker.js:41 [Service Worker] Caching all: app media content
fullStackOasis-PWA-service-worker.js:1 Uncaught (in promise) TypeError: Request failed

I had to add some debug statements to my ServiceWorker code to figure out what was going on with the confusing TypeError. It turned out that my code was buggy. Ouch! I had written code to try to cache an image file that didn’t exist, “media/thumb0000.jpg”. It was an edge case (the first element in a loop)! The first image file is actually “media/thumb0001.jpg”. I fixed the code which produced the array of image files to download, and that problem went away.

  1. At this point, the Console showed:
Navigated to
pwa.html:1 Site cannot be installed: Page has no manifest <link> URL

This message was pretty clear. I needed to install a manifest file.

Despite this error, I was able to run my web app offline! I tested this by opening DevTools, switching to the Network tab, and checking the “Offline” checkbox in the top menu. When I reloaded the page, the animation ran fine. Normally if you do this, you’ll see Google’s dinosaur page with the the ERR_INTERNET_DISCONNECTED message. So this was a small victory!

Aside from the “manifest” error, I noticed a few other errors in the DevTool Console when running the app offline:

The FetchEvent for "" resulted in a network error response: the promise was rejected.
fullStackOasis-PWA-service-worker.js:1 Uncaught (in promise) TypeError: Failed to fetch
/favicon.ico:1 GET net::ERR_FAILED
fullStackOasis-PWA-service-worker.js:60 Uncaught (in promise) TypeError: Request scheme 'chrome-extension' is unsupported
    at fullStackOasis-PWA-service-worker.js:60
An unknown error occurred when fetching the script.

My site didn’t have a favicon.ico file, which explains one of the errors. I believe the “chrome-extension” issue occurred because I have some extensions installed. I didn’t bother to check this. The “unknown error” was frustrating; I couldn’t figure out what caused it, although I didn’t spend very long trying.

These errors went away, once I replaced the “fetch” event listener that I had copied from Mozilla with the “fetch” event listener from a Google Developers page, later. The code which I was using from Mozilla was too rudimentary, apparently.

Make the PWA Installable

To make my PWA installable on a mobile device or on the desktop, it needed a manifest.json file. This file has plenty of optional properties, but some are required. Google Code Labs says “To be installable, Chrome requires that you provide at least a 192x192px icon and a 512x512px icon. But you can also provide other sizes…”. I made a small icon by grabbing one of the images from my “flip book”, trimming it so it was square in Gimp, and then using Roman Nurik’s handy, free Android app launcher generator. It generated all the icon sizes that I needed, and some extras as well.

My manifest.json is based on the one provided by Pete LePage at Code Labs. My manifest.json that I started with is shown below, but I’m going to warn you right away that it broke:

  "name": "Ain't Nature Grand",
  "short_name": "Nature",
  "icons": [{
    "src": "media/res/mipmap-hdpi/ic_launcher.png",
      "sizes": "72x72",
      "type": "image/png"
    }, {
      "src": "media/res/mipmap-mdpi/ic_launcher.png",
      "sizes": "48x48",
      "type": "image/png"
    }, {
      "src": "media/res/mipmap-xhdpi/ic_launcher.png",
      "sizes": "96x96",
      "type": "image/png"
    }, {
      "src": "media/res/mipmap-xxhdpi/ic_launcher.png",
      "sizes": "144x144",
      "type": "image/png"
    }, {
      "src": "media/res/mipmap-xxxhdpi/ic_launcher.png",
      "sizes": "192x192",
      "type": "image/png"
    }, {
        "src": "media/res/mipmap-xxxxhdpi/ic_launcher.png",
        "sizes": "512x512",
        "type": "image/png"
  "start_url": "/paw-2/pw.html?source=pwa",
  "display": "standalone",
  "background_color": "#eda342",
  "theme_color": "#e09636"

The Google Code Labs demo says to put your manifest.json file in a directory called “public” alongside your PWA page, and I did this. Here’s my directory structure:


At this point, I felt frustrated with the lack of clarity regarding the directory structure for PWAs. In the Code Labs example, all image “src” properties are declared with absolute paths, like “/images/icons/icon-512×512.png”. But it doesn’t say where those files are located on your server. Presumably, they are located under the web root (like “×512.png”). Using absolute paths is easier, but what if I don’t want to add images to my web root like that? What if I wanted my images to be relative to my web app path?

As you can see above, in fact, I used relative file paths to start, like “media/res/mipmap-mdpi/ic_launcher.png”. That’s what I preferred, but it turned out to cause problems, which I’ll come to soon.

Once I uploaded my icon images and manifest.json file to the server, I added this link tag to my PWA page:

<link rel="manifest" href="/pwa-2/public/manifest.json">

This type of link tag tells the browser that your page is installable. I added this tag to the header in pwa.html. You can see it in the source html file.

When I loaded my new page, I saw this in DevTools Console:

GET 404 (Not Found)
pwa-version4.html:1 Error while trying to use the following icon from the Manifest: (Download error or resource isn't a valid image)

This made it clear that if you use relative icon paths, they need to be relative to the directory where the manifest.json file is located.

I didn’t want to move my icon files into the public directory. So, I had to change the src values to be absolute, like this: "/pwa-2/media/res/mipmap-xxxxhdpi/ic_launcher.png". You can see the current manifest.json here.

After doing that, I reloaded the page. I opened DevTools > Application tab > Manifest, and saw that my icons had downloaded. Victory!

Let the User Install the PWA

At this point, the PWA is installable, but there’s no way for the user to know about it.

You need to add a button or banner or something which allows the user to request that the PWA be installed. However, you should not show this button until the browser fires a beforeinstallprompt event. Chrome won’t let you install the app until after this event has been fired.

So, I followed the instructions at Google’s Code Labs to listen for this event, display a button when it’s fired, and then show an Add to Home Screen (“A2HS”) prompt to install the PWA when the user clicks the button.

You can [view my source code][20] to see how my code compares to the Code Lab example.

Once I added this, my project came together. After loading the page, the animation started to play. Soon after that, the button I’d coded up appeared, giving the user the option to “install app”. Once this was clicked, I was able to install the PWA, which then showed up as an icon on my desktop. I could run it by clicking the icon, even when my desktop was offline.

I could also install the app on my cell phone, where it showed up in my Apps drawer. I could click it and add it to my home screen, and run it offline there, too.

Click here for the complete, final version of the PWA! Note: It takes a little while before the install button appears; you have to wait for the image files to download before this happens.

The complete web app - can be installed and runs offline

The complete web app – can be installed and runs offline

Tips and Gotchas for Debugging a PWA

While doing this work, I found a few things useful for debugging.

(1) <strong>console.log</strong> statements are your friend. 🙂

(2) Use the DevTools “Application” tab to completely remove the web application. Click to Clear Storage and you can use the “Clear Site Data” button to clear everything out. I used this a lot while developing this PWA. It seems a little bit buggy, however. Sometimes I’d use it repeatedly, and when I clicked to the “Service Workers” menu item in the left column, I’d still see multiple service workers registered (all of them had an “Unregister” link, and clicking that appeared to do nothing)! If I closed DevTools and reopened it, those empty Service Workers would be gone. Like I said – buggy area.

(3) Sometimes it helps to close the browser and reopen it if you do not see your changes being applied… This happened to me once, but I don’t recall the circumstances.

(4) Use the DevTools Audits panel. It lets you run an audit on your web app. There’s a specific section in the results called “PWA” which alerts you to some possible problems. And my PWA has some issues!

(4a) My site doesn’t redirect from http to https. That’s deliberate; otherwise I couldn’t demonstrate the problems with using http for a PWA.

(4b) There’s an error under the “Fast and Reliable” section: “Current page does not respond with a 200 when offline”. I don’t understand this error.

(4c) Another error says “start_url does not respond with a 200 when offline”.

I confess that I don’t understand (4b) and (4c). My app works fine. I used the Network tab of DevTools to take the site Offline, and all files downloaded with a 200 status code except for my fullStackOasis-PWA-service-worker.js, which responded with a 304 (Not Modified). The PWA installs without any problems. It runs even when I physically disconnect my ethernet cable from my computer. I didn’t have the time to debug this issue any further.

  1. If you have any trouble with beforeinstallprompt not being fired, you may want to investigate the Chrome flag chrome://flags/#bypass-app-banner-engagement-checks. I thought I might need this, but I never did.

A Word about the Code

In the spirit of the other source code I copied and pasted, and the video which I used, I release my code to the public domain, so if you want to copy it, feel free!

Note that it’s not production quality code. It still has some console.log statements in it, there’s inline css, and the JavaScript should be refactored. But it’s good enough for a demo. I hope this helped you in some way. If you have any questions, or need any help building a PWA, or converting your web app to one, contact me and I may be able to help! My email is