AMP is a way to build pages for static content that render fast. Progressive Web Apps (PWA) are reliable, fast and engaging experiences on the web. There are several ways in which a developer can use AMP components to build a PWA experience in their website.

What are we going to be building?

This codelab will walk you through implementing two different AMP + PWA patterns: 1) transforming an AMP site into a Progressive Web App, and 2) enhancing it using the App Shell architecture.

What you'll learn

What you'll need

Download the Code

Click the following link to download all the code for this codelab: Download. Alternatively, clone the project on Github.

git clone https://github.com/googlecodelabs/amp-pwa.git

Change into this working directory.

Install Node.js

This lab requires Node.js (and npm). Check the Node.js website on how to install it on your Operating System.

Install sw-precache

This lab uses sw-precache, a tool created by Jeffrey Posnick, to generate Service Worker code. Install this via npm:

npm install -g sw-precache

Install a web server on localhost

You will need to serve content from the local filesystem. Our tool of choice throughout the codelab will be serve, a Node.js based static content server. To install it, run:

npm install -g serve

Run the starter code

Test your setup to make sure you have all the pieces in place. Navigate to the directory with the codelab source code, then run the code in the work folder.

cd work
serve

Now, open Chrome and navigate to http://localhost:5000 and check if you are seeing our AMP index page.

Generate the Service Worker's code

We will start by using sw-precache to generate a service worker to cache static content such as logo images.).

On the root of the work folder, create a file and name it sw-precache-config.js. Add the following content to the file:

module.exports = {
  staticFileGlobs: [
    'img/**.*'
  ]
};

This will configure sw-precache to create a Service Worker that caches everything under the images directory.

Now, run sw-precache:

sw-precache --config=sw-precache-config.js

It will generate a service-worker.js script that is setup to cache images. This command should be run to regenerate the script every time an image is added or changed.

Install the Service Worker from AMP

Generating the service-worker script is only half the task. The next step is installing the Service Worker from the AMP pages. AMP provides the amp-install-serviceworker component to this end:

Add the Service Worker Javascript to the end of the head section of each of the AMP pages.

<script async custom-element="amp-install-serviceworker" 
  src="https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js">
</script>

We also need to add the component configuration to the bottom of AMP each page, right before the </body> tag:.

<amp-install-serviceworker 
  src="/service-worker.js" 
  layout="nodisplay">
</amp-install-serviceworker>  

Write a wrapper for the AMP Cache

AMP won't be able to install the Service Worker from the AMP Cache using the javascript file. In order to allow this, the amp-install-serviceworker component has an extra attribute called data-iframe-src that loads that URL as an iframe and allows the Service Worker installation from the AMP Cache.

Create /work/install-service-worker.htmlwith the following content:

<!doctype html>
<html>
  <head>
    <title>installing service worker</title>
    <script type="text/javascript">
        var swsource = "/service-worker.js";
        if("serviceWorker" in navigator) {
          navigator.serviceWorker.register(swsource)
            .then(function(reg){
              console.log('SW scope: ', reg.scope);
            })
            .catch(function(err) {
              console.log('SW registration failed: ', err);
            });
        };
    </script>
  </head>
  <body>
  </body>
</html>

This code will perform the Service Worker registration when this page is loaded from inside an iframe.

Return to the amp-install-serviceworker component in the AMP files and add a reference to this page:

<amp-install-serviceworker 
  src="/service-worker.js" 
  layout="nodisplay"
  data-iframe-src="/install-service-worker.html">
</amp-install-serviceworker>

Test it out

Make sure all files have been saved on your editor, and start a server by running serve.

Navigate to http://localhost:5000 using Chrome. Open Chrome Developer Tools by going to the Chrome Menu > More Tools > Developer Tools, or by using the shortcuts, Ctrl + Shift + I or Cmd + Option + I on a Mac.

Inside Chrome Developer Tools, navigate to the Applications tab and click on the Service Worker item.

You should see information that the Service Worker has been installed, like the above.

Congratulations!

You just took the first step towards transforming your AMP site into a Progressive Web App by creating and installing a Service Worker that caches static content. Now we want to cache dynamic content (such as each AMP page) when it's visited.

Cache visited pages

In order to cache pages the user has visited, we need to add another configuration to sw-precache-config.js. Add the code below, right after the staticFileGlobs attribute.

runtimeCaching: [{
  urlPattern: '*',
  handler: (request, values, options) => {
    // If this is NOT a navigate request, such as a request for
    // an image, use the cacheFirst strategy.
    if (request.mode !== 'navigate') {
      return toolbox.cacheFirst(request, values, options);
    }

    // If it's a navigation request, use the networkFirst strategy.
    return toolbox.networkFirst(request, values, options);
  }
}]

The runtimeCaching attribute receives an array of of handler configurations. This particular one is catching any request, and it's using the request.mode attribute to check if it's a request for a page or a request for a resource, and using different caching strategies for each.

Add a custom offline page

Now, when a visitor goes to a previously visited page while offline, they will be able to see a cached version of the page. But if the user clicks on a link that wasn't previously visited, they will still get the offline page. Using a Service Worker, it's possible to customise the offline page:

Create offline.html and add this simple offline page

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>The Photo Blog - Offline</title>
  <meta name="viewport"
        content="width=device-width,minimum-scale=1,initial-scale=1">
</head>
<body>
  <h1>You are Offline</h1>
</body>
</html>

Edit sw-precache-config.js. First, add the offline page to the list of static files to cache on the staticFileGlobs:

module.exports = {
  staticFileGlobs: [
    'img/**.*',
    'offline.html'
  ],
...

We already have a call to toolbox.networkFirst on our handler, that returns a Promise. This Promise is rejected if the if the network is not available and the file is not found on the cache. This a perfect time to serve up the offline page instead.

Add a catch section after the toolbox.networkFirst method call to return the offline page when a request fails:

...
if (request.mode !== 'navigate') {
  return toolbox.cacheFirst(request, values, options);
}

// If it's a request for content, use the networkFirst
// strategy, and send an offline page if both network and
// cache fail.
return toolbox.networkFirst(request, values, options)
  .catch(() => {      
    return caches.match('/offline.html', {ignoreSearch: true});
  });      
...

Cache the AMP runtime

If you try to open one of the AMP pages offline, you will see a bunch of errors on the console:

While the pages and supporting content were cached, the AMP runtime wasn't.

Edit sw-precache-config.js to append the following to the runtimeCaching array:

{
  urlPattern: /cdn\.ampproject\.org/,
  handler: 'fastest'
}

This will cache the AMP Runtime using the fastest strategy, This strategy will request the resource from the cache and network in parallel. The first response will be used (usually the cached version, if available. This strategy also ensures the cache is updated with any newer version of the AMP runtime, so it will be served on the next request.

The full sw-precache-config.js should look like this:

module.exports = {
  staticFileGlobs: [
    'img/**.*',
    'offline.html'
  ],
  runtimeCaching: [{
    urlPattern: '*',
    handler: (request, values, options) => {
      // If this is NOT a navigate request, such as a request for
      // an image, use the cacheFirst strategy.
      if (request.mode !== 'navigate') {
        return toolbox.cacheFirst(request, values, options);
      }

      // If it's a request for content, use the networkFirst
      // strategy, and send an offline page if both network and
      // cache fail.
      return toolbox.networkFirst(request, values, options)
        .catch(() => {      
          return caches.match('/offline.html', {ignoreSearch: true});
        });      
    }
  }, {
    urlPattern: /cdn\.ampproject\.org/,
    handler: 'fastest'
  }]
};

Don't forget to regenerate the Service Worker by running:

sw-precache --config=sw-precache-config.js

Test it out

Navigate to http://localhost:5000 and wait for the page to load.

Open Chrome Developer Tools and navigate to the Application tab. On the Cache section, right click on Cache Storage and click refresh. Check the caches and verify if the files necessary for the page are listed in the cache.

On the Service Worker section, click on the Offline checkbox and reload the page. You should see that the page loaded even though it's offline. Now, click on one of the links to articles. Since the article wasn't cached you should see the custom offline page.

One of the ways a PWA resembles a native application is it can be added to the Home Screen. Adding a Web Manifest enables this (on Chrome; Safari uses <meta> tags and Firefox has its own manifest format.)

Those are the manifest attributes that make an app eligible to be added to the Home Screen, at the time of this writing:

Generate the Web Manifest

Writing a Web Manifest by hand can be an error prone task. There are several online manifest generators available on the web that can help with this task. We are going to use the one at https://app-manifest.firebaseapp.com/ to generate ours.

Fill the form fields with the desired values. Here's what we are using for this codelab:

The next step is upload the icon to the generator. Click on the ICON button, navigate to the work folder and upload the icon on /icons/web_hi_res_512.png.

Click on the GENERATE ZIP button to download icons generated in multiple sizes and the full manifest.

Extract the downloaded zip file and copy its contents your project, so that the manifest.json file is on the root of the work directory.

Link the manifest

Since the user might start from any AMP page, we need to link to the manifest from each page.

Add a link tag to the head of each AMP page:

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

Makeing sure start_url is cached

The URL referenced on start_url must always be available, even when the user is offline. We can pre-cache the url when the Service Worker is installed and use the networkFirst cache strategy to ensure it's always fresh.

Since the index.html page is of a more dynamic nature, when compared to the an offline page, we need to use a different approach to pre-cache the page. We'll take advantage of sw-precache's extensibility to do it.

On the sw-precache-config.js file, add another configuration property:

importScripts: ['service-worker-import.js']

This will add an importScript call to the generated Service Worker. Now, create a service-worker-import.js file on the root folder, and add this line of code:

toolbox.precache(['/index.html']);

When the imported script is invoked by the generated Service Worker, it will add the index.html page to the runtime cache. By doing this, we ensure the page will be available for the existing handler we created earlier.

Run sw-precache to update the generated Service Worker:

sw-precache --config=sw-precache-config.js

Test it out

On Chrome Developer Tools, navigate to the Application tab and check the Manifest item under the Application section. The information contained on the Manifest should be displayed on the right side.

Extra Credits

You may have noticed that, although the index.html page has been cached, the images for the articles were not. Although it is possible to pre-cache the content, the dynamic nature of the index page makes it harder to know which images to cache. A possible solution for this is to serve a fallback offline image, when the requested one is not available. This is an exercise to the reader.

The AMP as Canonical PWA

Congratulations, you've just implemented your first AMP as Canonical PWA. You have made your application more resilient to offline scenarios, optimized loading performance by caching static assets and images and became eligible to be launched from the home screen!

Although the AMP as a Canonical PWA architecture may be enough in many scenarios, developers may need to use features that are not yet supported in AMP, such as Push Notifications or the Credentials Manager API.

For those scenarios, the AMP in a PWA architecture can be used.

Replacing requests with the shell

Let's start by adding an App Shell to our application. We will then change our Service Worker, so that once installed, it will catch any navigation requests and replace them with the shell.

Create a file called shell.html in the work folder:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">  
    <title>AMP => PWA Demo</title>
    <style type="text/css">
body{margin:0;padding:0;background:#F5F5F5;font-size:12px;font-weight:300;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}a{text-decoration:none;color:#000}.header{color:#fff;background:#1976D2;padding:8px 16px;box-shadow:0 2px 5px #999;height:40px;display:flex;align-items:center}.header h1{margin:0 8px 0 0}.header amp-img{margin-right:8px}.header img{margin-right:8px}.header a{color:#fff}.header a:visited{color:#fff} 
    </style>
  </head>
  <body>
    <header class="header">
      <img src="/img/amp_logo_white.svg" width="36" height="36" />
      <h1><a href="/">AMP PWA Codelab - PWA</a></h1>
    </header>    
    <div id="amproot">
      <!-- AMP Content should appear here! -->
    </div>
  </body>
</html>

Update the sw-precache-config.js to serve the created shell.html for navigate requests. Here's how the full config should look like:

module.exports = {
  staticFileGlobs: [
    'img/**.*',
    'offline.html',
    'shell.html',
    'js/**.js'
  ],
  runtimeCaching: [{
    urlPattern: '*',
    handler: (request, values, options) => {
      // If this is NOT a navigate request, such as a request for
      // an image, use the cacheFirst strategy.
      if (request.mode !== 'navigate') {
        return toolbox.cacheFirst(request, values, options);
      }

      return caches.match('/shell.html', {ignoreSearch: true});  
    }
  }, {
    urlPattern: /cdn\.ampproject\.org/,
    handler: 'fastest'
  }],
  importScripts: ['service-worker-import.js']
};

The main handler was changed, so that any navigation request replaced with the App shell.

Loading AMPs with amp-shadow

For this step, you will be using the a javascript file, located at js/app.js, inside the work directory. This file already contains some boilerplate code needed for the shell to word: a method to fetch AMP documents from the backend and a Promise that allows to schedule code to be ran when the amp-shadow component is loaded.

To get started, add the amp-shadow component to the head section of the App Shell:

<!-- Asynchronously load the AMP-Shadow-DOM runtime library. -->
<script async src="https://cdn.ampproject.org/shadow-v0.js"></script>    

At the bottom of the shell, import the js/app.js script:

<script src="/js/app.js" type="text/javascript" defer></script>

Now, open the app.js script and add code that to set up the variables that are needed to add the AMP content to our amproot div.

const ampRoot = document.querySelector('#amproot');
const url = document.location.href;
const amppage = new AmpPage(ampRoot, router);
ampReadyPromise
  .then(() => {
    amppage.loadDocument(url);
  });

The original request was replaced by the Service Worker with the content from shell.html, but the URL is still the same. We need to fetch the content from this URL to inject into our AMP root. So, we wait for the ampReadyPromise to resolve and load the content from the URL.

Let's implement the loadDocument method. You should find the placeholder for it a few lines above.

loadDocument(url) {
  return this._fetchDocument(url)
    .then(document => {
      router.replaceLinks(document);
      window.AMP.attachShadowDoc(this.rootElement, document, url);            
    });       
}

The document is fetched from the URL, and once it's resolved we call the amp-shadow API to attach it to the document.

Manipulating the content of the AMP file

When injecting the AMP pages in the App Shell, it's likely that developers will need to add, remove or change sections of the AMP document.

The fetched document is a regular DOM document, so a developer can manipulate it as needed. On the snippet below, we are removing the Header element from the AMP document, as the header is already present on the App Shell and ends up being duplicated.

loadDocument(url) {
  return this._fetchDocument(url)
    .then(document => {
      const header = document.querySelector('.header');
      header.remove();
      router.replaceLinks(document);
      window.AMP.attachShadowDoc(this.rootElement, document, url);            
    });       
}

Test it out

Open Chrome and navigate to http://localhost:5000. When opening the first page, the browser should render the AMP page, as the Service Worker is not yet installed. By reloading the page, the App Shell version should be rendered.

In the current implementation, when the user navigates the site on a browser that doesn't support Service Workers, they will never see the App Shell experience. Fortunately, the amp-install-serviceworker component provides a fallback that rewrites links on the page to the shell URL.

On the AMP pages, update the amp-install-serviceworker component to include the fallback attributes:

  <amp-install-serviceworker 
    src="/service-worker.js" 
    layout="nodisplay"
    data-iframe-src="/install-service-worker.html"
    data-no-service-worker-fallback-url-match=".*"
    data-no-service-worker-fallback-shell-url="/shell.html">
  </amp-install-serviceworker>  

Now, when the user using a browser that doesn't support Service Workers clicks on a link inside the AMP files, the AMP runtime will replace the link with the shell URL and append the original URL as a fragment, in the format #href=<original url>.

So, a link to /articles/1.html becomes /shell.html#%2Farticles%2F1.html. Let's make our App Shell code aware of this change. Add a method to find out the correct url for the content

...
function getContentUri() {
  const hash = window.location.hash;
  if (hash && hash.indexOf('href=') > -1) {          
    return decodeURIComponent(hash.substr(6));
  }
  return window.location;  
}
...
const ampRoot = document.querySelector('#amproot');
const url = getContentUri();
const amppage = new AmpPage(ampRoot, router);

If the user is coming from the service worker, the url fragment will be present, so we can extract the final URL using decodeURIComponent. If the content of the URL was replaced by the Service Worker, we already have the correct URL. The assignment to the url variable is replaced with a call to the new method.

Fixing broken URLs

A side effect of using the URL Rewrite fallback is that when a user clicks on a link inside the shell, the URL in the url bar will show the shell url with the fragment. We can use the Page History API to fix it.

ampReadyPromise.then(() => {
  return amppage.loadDocument(url);
})
.then(() => {
  if (window.history) {
    window.history.replaceState({}, '', url);
  }
});

Once the loadDocument Promise successfully resolves, we replace the shell URL with the one that was just loaded.

Test it out

Start a browser that doesn't support Service Workers. At the time of this writing, examples are Edge on Windows and Safari on OSX. Navigate to http://localhost:5000. The first view should be an AMP page. It's possible to verify it by opening the Developer Tools on that browser and checking the source code. Now, click on one of the links on the initial page. The URL should have been rewritten, and the page being rendered using the App Shell.

Congratulations!

You've just finished implementing your first AMP in a PWA. Your application has all the advantages of the AMP as a Canonical PWA pattern, and you are now able to add features that were only possible in AMP.

What's Next?

This codelab only scratches the surface with regards to with what is possible when building Progressive Web Apps with AMP. When building a more complex experience, be sure to check the application design against the PWA Checklist. Also, check Your First Progressive Web App codelab, to learn the more about the details on how the Service Worker works.