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. 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 (available at chrome://apps, copy and paste this into your browser) to serve the work folder on port 8887 - unless you've changed it. Either way, write this down or remember it for later.

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 your mobile phone. 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.

The viewport

We won't cover the viewport here, but you can read more about it on your favourite search engine. Needless to say, when you reload the site on your phone now, you should see it 'fit to size'. This change works on its own because the site was already designed to work on screen sizes of 320px or smaller.

The manifest

The Web App Manifest is needed to control how your site is added to a user's home screen. It supersedes older meta tags like mobile-web-app-title. So, in your code, you've referenced the file - but 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": "standalone",
  "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!

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 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! */

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, 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 automate the sending of messages in this part, but let's register for them and you'll be able to send them via the command-line.

As of Chrome 52, you'll need to obtain what's called a gcm_sender_id in order to be authorized to send messages to devices. This process is a pain! You should follow these steps which are available on Google Developers. Once you have the ID - which is the project ID of that project you just created - return here and continue onwards.

Update your manifest

Add the gcm_sender_id to your manifest. It should look like this-

{
  "name": "The Most Awesome Dragon Site",
  "short_name": "🐉🐉🐉",
  "gcm_sender_id": "YOUR_GCM_ID",
  /* ... lots of other stuff */
}

Subscribe to push

Now, inside your site's code, you'll need to subscribe to push messaging. It's actually really 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);
        return;  // got one, yay
      }
      serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
        .then(function(subscription) { 
          console.info('Newly subscribed to push!', subscription);
        });

You should now be able to save, and reload your page - either on desktop or mobile, it doesn't matter. If you check the developer console, 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.

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 visible page so this is added to your site.

Putting it together

Next, open up a command-line prompt and use curl to construct a carefully-crafted HTTP request.. This will cause your device to wake up and run the code we just added, to show a notification.

You'll need two parts here. Go and find that endpoint URL from before. Expand the console if required, and then copy everything after gcm/send/ - the long random token. That goes where REGISTRATION_ID is listed below.

Next, use your Server Key from the setup flow before where YOUR_KEY is listed.

$ curl -d "{\"registration_ids\":[\"REGISTRATION_ID\"]}" --header "Authorization: key=YOUR_KEY" --header "Content-Type: application/json" https://android.googleapis.com/gcm/send

And, presto

You should see something like this arrive on your client - desktop or mobile. 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, also 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

That's not great, especially for testing. However, it's configurable. Some higher-level options are sw-toolbox or platinum-sw.

  1. Load the Developer Tools (with ⌘-⌥-J, or Cmd-Alt-J), then either-
  1. Under the Application tab, ensure everything is ticked then click "Clear selected" - this will work on Chrome 52 or later
  2. Under the Resources tab, and Cache Storage, look for "your-magic-cache" - the cache name from before - and right-click on it to delete.
  1. Load http://localhost:8887/clear.html, and press the button. If you're trying this on your phone, this might be easier!

This can be frustrating to do on every reload of your page. It's the reason we've left this offline step until last. If you don't have a clear caching strategy, this can have negative effects for your users - they may never get a new version of your site. It's also often useful to disable the fetch event completely during development, so you'll always get the latest version of the site.

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 here at Google I/O, which will go into more detail on all of these topics. Or read more on Service Worker and Push Notifications.

🤘