Progressive Web Apps are well and good, but how do I get there from where I am now? Let's migrate an existing, desktop-only site to support amazing new web features like install, push and offline. These features will make your sites more accessible and compelling to your users on their increasingly mobile devices—and they'll also make your desktop users more engaged.

What you'll learn

What you'll need

Where possible, this codelab takes the fastest path to these changes while deferring to more complex resources and other codelabs for a more in-depth explanation. And by taking these steps on a simple site as part of this codelab, you will learn how to apply them to your own sites.

The starting point

You'll be modifying a site named Dragotchi, where you can play with and feed a pet virtual dragon. This site has three pages (Home, Dragotchi and FAQ). Two are static, but the Dragotchi page makes simple Ajax requests to a public server.

This site is desktop-only, does not have a responsive design, and is generally a bit out-of-date. That's fine! We can take just a few steps to improve it.

A note on length

You should complete as much or as little of this codelab as you like! Every single page will teach you something new that you can use, and if you want to stop early, that's fine too.

Let's get started by checking out the Dragotchi site and setting up your work environment.

What you'll need

Clone the GitHub repository from the command line:

$ git clone https://github.com/googlecodelabs/migrate-to-progressive-web-apps.git

Once the code is checked out, you should use the Simple HTTP Server application (or e.g., a local Python server or other HTTP server) to serve the work folder on port 8887.

You can now load that URL and play with the 🐲. Don't worry, the codelab will wait.

Viewing the site on mobile

If you have an Android device plugged in, you can (on your desktop) head to the URL: chrome://inspect (you'll need to copy and paste this). Then, set up a port forward using the port you wrote down before to the same port on the device. Hit enter and this will save.

Now, you should be able to access the basic version of Dragotchi at http://localhost:8887/ on a connected Android device. The version on your mobile phone isn't optimized for that display—we'll get to that in the next step.

The first thing we'll do to this old-timey site is to make it mobile-friendly and include what's called a Web App Manifest. This manifest file describes meta information about a site, such as how it might look when added to a user's home screen.

The Dragotchi site has three HTML pages. As it doesn't use any sort of templating system, you'll need to add the following lines to the <head> tag each of them—index.html, faq.html and dragon.html-

<head>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
</head>

If you're short on time, you could just add these tags to the index.html page. But, be aware that in real sites, you'll need to specify this code everywhere, maybe in a common template.

The viewport

The first line is the meta tag, specifying viewport. You can read more about it on your favourite search engine, but for now, if you reload the site (either on your Android device, or under Developer Tools) you'll see the page be correctly sized to the page.

The Web App Manifest

The second line adds a Web App Manifest, which is needed to control how your site is added to a user's home screen. In your code, you've referenced the file: let's now create it!

Open up a text editor. We're going to write some JSON. Start with something like this-

{
  "name": "The Most Awesome Dragon Site",
  "short_name": "🐉🐉🐉",
  "display": "minimal-ui",
  "start_url": "/",
  "theme_color": "#673ab6",
  "background_color": "#111111",
  "icons": [
    {
      "src": "icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ]
}

The short_name field specifies exactly what will appear on a user's home screen: try to keep this short, as names >15 characters can be truncated. In our case, we've used 🎆 emoji 🌋 because that's totally valid!

Some things to remember here-

Save the file as manifest.json. Reload the page on your Android device, head to the upper-right overflow menu, and select "Add to Home Screen". You should see a brand new dragon application on your home screen!

If you don't have an Android device handy, select the "Application" tab inside Chrome Developer Tools. Select "Manifest" to see the details of the manifest. If you see "No manifest detected", then make sure you created the manifest.json file and saved it inside the 'work' folder.

Whoa! That's awesome!

I know! Let's move on and add a Service Worker!

The Service Worker is a background script that a browser can run even while a user isn't on your page. It's the thing that provides offline support, and wakes up when a notification is triggered.

Even having an effectively empty Service Worker can help your site. How can I get this amazing thing? Well, read on.

Add the Service Worker code

First, we're going to write a new JavaScript file. Let's copy the bare minimum code into a new file-

/** An empty service worker! */
self.addEventListener('fetch', function(event) {
  /** An empty fetch handler! */
});

Save this as sw.js! That's it, you're done! You've created a service worker! 🎆

Register the Service Worker

Oh, there's another step? Well... okay. The above code and file (sw.js) isn't referenced anywhere. You'll have to register it imperatively, i.e., inside your site's code.

The Dragotchi site already includes a script that's run by every page in the site. Open up site.js (another file that starts off empty), and add the registration code-

navigator.serviceWorker && navigator.serviceWorker.register('./sw.js').then(function(registration) {
  console.log('Excellent, registered with scope: ', registration.scope);
});

That's it! This code will execute on every page load. If the Service Worker is actually already registered, your browser will ignore the request and check if it's changed at some later point.

Be sure to reload your page and then check chrome://serviceworker-internals/ to ensure that it's actually loaded for the site. It should look like this-

Great, but what can I do with this?

Firstly, and perhaps most excitingly, your site will now prompt users to install it to their home screen, if they've shown a certain level of engagement. To find out more, including how to always show the prompt during development, read about it on Google Developers.

Secondly, you can expand the Service Worker to make your site support push notifications and work offline. Read on!

Let's support Push Notifications! We aren't going to build a server to send messages, but let's register for them on your site, and you'll be able to try them out via the command-line.

You'll need to generate a pair of public and private keys. Head to the Push Companion site and keep it open—it generates a pair of keys for you, and we'll use them both shortly.

Subscribe to push

Inside your site's code, you'll need to subscribe to push messaging. It's fairly simple. Let's add code that looks for a subscription to site.js-

navigator.serviceWorker && navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {  
  serviceWorkerRegistration.pushManager.getSubscription()  
    .then(function(subscription) {  
      // subscription will be null or a PushSubscription
    });
});

If the subscription provided is null, then we need to go register ourselves for one, so replace the comment with this-

      if (subscription) {
        console.info('Got existing', subscription);
        window.subscription = subscription;
        return;  // got one, yay
      }

      const applicationServerKey = urlB64ToUint8Array(publicKey);
      serviceWorkerRegistration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey,
      })
        .then(function(subscription) { 
          console.info('Newly subscribed to push!', subscription);
          window.subscription = subscription;
        });

Firstly, note that publicKey isn't defined anywhere. Create it at the top of the file, using the public key from Push Companion earlier-

const publicKey = '<public key from Push Companion>';

You should now be able to save, and reload your page - either on desktop or your Android device, it doesn't matter. If you check the Console inside Developer Tools, you'll see a log message containing the subscription, which contains an "endpoint" field. We'll use this in a second, so leave this window open.

If you see an error, make sure your publicKey looks like a long base64-encoded string, not "<public key from Push Companion>".

Show a notification

It's all well and good to subscribe to push, but what should your site do when it receives a message? As your user might not have Dragotchi loaded all the time, you'll need to add code to handle the message in your Service Worker. If you remember from before, it can run anytime.

Let's open sw.js and add this section-

self.addEventListener('push', function(event) {
  event.waitUntil(
    self.registration.showNotification('Got Push?', {
      body: 'Push Message received'
   }));
});

This is the one of the simplest notifications we can show. It has a title and a simple body. For more options, check out some documentation. For now, reload the page so this is added to your site.

Putting it together

Head back to Chrome Developer Tools on your desktop (use ⌘-⌥-J or Ctrl-Alt-J to open it, if you haven't already). We're going to use a helper, included with the project, to generate a command you can run on your command-line to trigger a push notification. You'll need the "curl" helper, installed on Mac or Linux by default.

Copy the following code and paste it into the console inside Developer Tools, as if you were running the code by typing it in yourself-

var privateKey = window.prompt('Enter Private Key from Push Companion');
privateKey && prepareAuthorization(publicKey, privateKey, subscription.endpoint, 'mailto:example@example.com').then(auth => {
  const curl = `curl "${subscription.endpoint}" ` +
      `-X POST ` +
      `-H "Authorization: ${auth}" ` + 
      `-H "Crypto-Key: p256ecdsa=${publicKey}" ` +
      `-H "TTL: 100" ` +
      `-H "Content-Length: 0"`;
  console.warn('Copy and paste the following into a command-line-');
  console.dir(curl);
});

This will immediately prompt you for the private key from Push Companion. Enter it. Once you've hit Ok, the console will display a very long line. Go ahead and double-click on it, copy it, and paste it in a handy command-line type enter to run it.

And, presto

You should see something like this arrive on your desktop. Good job!

This is just scratching the surface. The endpoint you get back on the client could be sent up to your backend or other service that can trigger push notifications at any time, even when your site isn't visible.

Let's use this new Service Worker to make sure that our Dragotchi site works offline. First, let's open up the sw.js script again. First, we want to get hold of the caches object, which is of type Cache-

self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open('the-magic-cache').then(function(cache) {
    });
  );
});

Now, once we've got hold of that, let's update that code to add our entire site to the cache-

self.addEventListener('install', function(e) {
  e.waitUntil(
    caches.open('the-magic-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/dragon.html',
        '/faq.html',
        '/manifest.json',
        '/background.jpeg',
        '/construction.gif',
        '/dragon.png',
        '/logo.png',
        '/site.js',
        '/dragon.js',
        '/styles.css',
      ]);
    })
  );
});

This list is pretty long, but it's important. If you have some build process for your site, perhaps that could generate the list of possible URLs that a user might load.

Matching requests

This is all well and good, but right now you're just caching contents when a user installs your app. You're not actually responding to HTTP requests with the contents of that cache - that's a different step! Let's do the basic work, and replace the existing 'fetch' handler inside sw.js-

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

The Cache object will match your request, returning a valid or null response. If it's null, then we defer to the fetch method to actually do the real request.

You could write more complicated matchers. These two code blocks represent a simple cache - you store assets directly by URL and return them by URL. However, there's nothing stopping you running more complicated JavaScript - we'll touch on that in the next step.

Trying it out

Head over to your phone, "uninstall" the existing Dragotchi app by long-pressing on it and removing it. Then, load it up in Chrome, refresh the page, and head to the upper-right overflow menu, and select "Add to Home Screen".

If you now disconnect the Android device's USB cable, and tap on that home screen icon - Dragotchi should just load!

Some caveats

This cache, while great, will cache your site forever!

Until the end of time!*

*or user's disk runs out

This isn't great. You should look into Workbox, which is a Google-released set of libraries to help you make Progressive Web Apps and write better Service Worker code.

For now though, load up http://localhost:8887/clear.html and press the "Clear" button. This page is a testing page provided in the source code that helps you to clear the cache created by the Service Worker's "install" event.

This can be frustrating to do on every change page. It's the reason we've left this offline step until last—it's surprisingly easy to get into a state where the page "never changes" because your cache is not cleared.

The basic rule is: if your Service Worker changes, your page will reload it, and reinstall it. During development, you could add a simple comment that contains the "version" of your service worker. Whenever it changes, the install event will occur again, caching whatever other resources you might have changed. Add the comment now, at the top of the sw.js file-

// version: I'm nearly finished the codelab woo!

You can imagine that rather than doing this manually, it might appear as part of a build step.

Next

So, we've kind of cheated - if you load the dragon page, and pet the dragon by clicking one of the actions, this works but only because your phone is still online. This request is handled by an external server provided for this codelab - this is outside your site, a real URL out there on the internet!

You can see this if you put your phone in Airplane Mode, you'll get a loud alert of failure. Let's fix this.

As we've alluded to, there's much more we can do with the Cache available in Service Worker. Let's handle a new URL. The state of your pet dragon comes from an external site, so we can just intercept that.

Inside the fetch block in sw.js, let's look for requests to a specific URL-

self.addEventListener('fetch', function(event) {
  if (event.request.url == 'https://dragon-server.appspot.com/') {
    return;
  }
  /** previous code */
});

This shows that a Service Worker can actually intercept requests to URLs outside your site's domain. Now, let's do something if we see that URL-

  if (event.request.url == 'https://dragon-server.appspot.com/') {
    console.info('responding to dragon-server fetch with Service Worker! 🤓');
    event.respondWith(fetch(event.request).catch(function(e) {
      let out = {Gold: 1, Size: -1, Actions: []};
      return new Response(JSON.stringify(out));
    }));
    return;
  }

This will return a valid, but limited state when calls to this URL are made that fail (see the catch block of this Promise). This codelab is intended as a quick introduction - so there are a lot more options here, including the cache, or storing state offline in something like indexDB.

The key here is to demonstrate that you have complete control.

Trying it out

As per the previous page, be sure to clear the cache. Head to http://localhost:8887/clear.html to clear the cache. Then, remove the app from your home screen, reload it in Chrome, and then install the app again.

Set Airplane Mode, then load the site from that icon, and head over to the Dragotchi page. You'll see the default state that you've provided from the Service Worker, and that it was handled there!

Congratulations

You've learned a whole bunch of interesting steps to make your great sites a little bit more progressive, even for those sites grounded a little bit in the past!

Now, be sure to try out many of the other codelabs, which will go into more detail on all of these topics. Or read more on Service Worker and Push Notifications.

🤘