April 7th, 2017

Three things to consider before your progressive web app goes standalone

I was at TECH.insights in London last week, and Rowan Merewood gave an excellent talk on progressive web apps. It was one of these presentations including tons of information so that even people that are familiar with the technology could learn something new. I love this kind of talks!

My personal website is a collection of static HTML files and is also a progressive web app. Transforming it into a progressive web app felt a bit weird in the beginning because it's not an actual application but I wanted to be one of the cool kids, and PWAs still offer a lot of additional improvements. Advanced caching and offline behavior are things that are useful for any website out there.

The implemented service worker uses sw-toolbox to make dealing with a certain caching strategy easier. If you haven't tried sw-toolbox yet, you definitely should! It's a great help and makes a service worker implementation very easy.

Let's see how my site looks Chrome on my Android device and take this as a start to make it a standalone web app.

Making the app standalone

One of the cool things about progressive web apps is that they are "installable" and you can get rid of the browser UI around your web application. You can achieve this by linking a manifest.json file in your HTML.

<link rel="manifest" href="manifest.json">
{
  "name": "Stefan Judis Web Development",
  "short_name": "Stefans Discoveries",
  "theme_color": "#1d7cbb",
  "background_color": "#ffffff",
  "display": "standalone",
  ...
}

To make your web app feel like a real app you can use the display property and set it to standalone to make the app look and feel like a standalone application. This option is very exciting – but let's have a look at what this means.

The browser-UI is there for a reason

Even when it feels nice to see your website in standalone mode you have to be aware of the fact that crucial features of the browser UI are gone then. You can check the standalone version of my site below. It clarifies three main issues.

Progress indicators are a crucial part of a good user experience

The first thing I noticed is that progress indicators are gone. In a standard browser UI, there is always something telling you that you hit a link and that you are waiting for the responses to come back. In standalone mode, there is no visual indicator anymore. Well developed single page applications have progress indicators implemented, but "normal" websites like my personal one usually haven't.

A missing progress indicator becomes very noticeable when you're on a slow connection. When I was over in the UK last week, I found myself hitting links on the phone several times because I was not sure if something was going on.

How to fix missing loading indicator?

It turns out you can detect if your site is running in standalone mode by using window.matchMedia. My first idea was to show a loading indicator whenever someone hits a link.

var baseRegex = new RegExp( `${ window.location.hostname}` );

if ( window.matchMedia( '(display-mode: standalone)' ).matches ) {
  window.addEventListener( 'click', function() {
    if (
      event.target.tagName === 'A' &&
      ! baseRegex.test( event.target.href )
    ){
      document.getElementById( 'loading-indicator' ).classList.add( 'is-active' );
    }
  } );
}

This solution felt a bit weird but worked fine and shows that something is in progress. 🎉

Different origins stay inside the standalone mode

The next problem is links to different origins. I don't want to open the almost religious discussion of links and target="_blank" here, but I don't like the rule that links going to a different origin should open in a new tab. I'm a classical "Tab-Hoarder" and would like to decide by myself when to open a new tab. That's why there are no target="_blank" links on my site.

To fix this issue, I extended the previous JavaScript snippet to call window.open when the link points to a different origin.

var baseRegex = new RegExp( `${ window.location.hostname}` );

if ( window.matchMedia( '(display-mode: standalone)' ).matches ) {
  window.addEventListener( 'click', function() {
    if (
      event.target.tagName === 'A' &&
      ! baseRegex.test( event.target.href )
    ){
      window.open( event.target.href );
      event.preventDefault();
    } else {
      document.getElementById( 'loading-indicator' ).classList.add( 'is-active' );
    }
  } );
}

Calling window.open will leave the standalone UI and open the default browser. Great!

URLs become unshareable

The last and final issue is that users can not access URLs anymore. You saw in the video that I visited a blog post, and there is no way to share the location or even to see what the actual location is.

One of the core principles of the internet is that we can link resources. I didn't want to break with that. But how can this work when visitors can't access a URL?

Side note: Yesterday I added the new Twitter Lite app to my home screen and I was surprised to discover that the app has the same problem.

Accessible URLs are a core feature of the web!

To get around this issue, I implemented the Web Share API. The Web Share API is still an experimental feature, and there are some gotchas:

  • You need to host your site in a secure context
  • You need to supply at least one of text or URL but may supply both if you like
  • You can only invoke the API as a result of a user gesture
  • The property values that you pass into the API must all be strings

I can live with all these. Another catch is, that if you want to use the Web Share API, you still have to sign up for origin trial and place a meta tag in your documents to use it. Then you're ready to trigger real share dialogs on a button click for example. That's great. In case you want to dig deeper Michael Scharnagl wrote an excellent tutorial on this topic and explained how to implement it in a progressively enhanced way.

// check if Web Share API is available
if ( navigator.share !== undefined ) {
  var SHARE_BTN = document.querySelector( '.shareBtn' );

  // change link from "Share on Twitter" to "Share"
  SHARE_BTN.innerHTML = `<svg>...</svg> Share`;

  SHARE_BTN.addEventListener( 'click', function (ev) {
    navigator.share( {
      title : document.title,
      url   : window.location.href
    } )
    .then( () => console.log( 'Successful share' ) )
    .catch( ( error ) => {
      console.log(' Error sharing:', error );
      window.open( this.href );
    } );

   ev.preventDefault();
  } );
}

But wait for a second and let's think this through for a moment. When you're in a native app, the share functionality is usually app specific, and users learned where to find the share-button. For the case of my website – I don't want to place a very prominent share button somewhere, and I also don't want to teach users how to share things. This approach doesn't feel right for a static site.

In the end, I went with a share button at the end of the article pages. This button works more or less fine, and I love the Web Share API, but in my opinion, that's far away from being optimal because not every page is shareable in standalone mode then.

Side note: It looks like Chrome Desktop has a problem with navigator.share in the current version. I'm still investigating this issue. :)

Reimplementation of these features

So let's have a look at the final result.

This result is more or less acceptable, but honestly, I wasn't satisfied with this result.

For static sites it's not worth it

What I did is that I reimplemented features that I get for free when my site runs in a browser. And let's be clear here – my site is not a web app. Is it worth it to reinvent the wheel?

After sleeping a few nights over it, I decided that standalone mode is not the best fit for my static site. I switched to "display": "minimal-ui" which isn't supported today but falls back to the normal browser UI. I can live with that and am curious how the minimal UI tackles the described problems. I removed the "standalone app JavaScript", too.

My site still benefits from the progressive web app goodies and doesn't run in the standalone mode in case anybody added it to his/her home screen. And it doesn't feel like an app – but I think that's okay!

Any comments?