React-static PWA — Tutorial Part III

Christian Kaatz
ITNEXT
Published in
8 min readDec 17, 2020

--

03 React-Static-PWA

As we now have an App and Continuous Deployment in place, we want to improve our app by adding PWA Capabilities. This will add functionality, to have our website be installable through a browser (Chrome, Safari, Firefox…) and usable like a native app with its own app icon and shortcuts.

Check out the current state at app-bootstrap-tutorial.web.app

App on an Android device

Basic Functionality

First of all, we need a PWA manifest which describes our application and will tell the OS/Browser the name, display behaviour, icons and other things like shortcuts.

We will create a basic manifest.json in our public directory.

{
"short_name": "the app",
"name": "the-app-tutorial",
"description": "App to display PWA capabilities with react-static and grommet",
"start_url": "/",
"background_color": "#000000",
"display": "standalone",
"scope": "/",
"theme_color": "#000000",
"shortcuts": [],
"icons": []
}

Also we need to update our App.js in the src folder:

// replace the following line with the one below
// import { Root, Routes } from "react-static";
import { Head, Root, Routes } from "react-static";
// ...
function App() {
return (
<Root>
// only add it when we build our app for production
{process.env.NODE_ENV === "production" && (
<Head>
<link rel="manifest" href="/manifest.json"></link>
<script src="/init-sw.js"></script>
</Head>
)}
// ...

As you’ve seen, we also need to create another file: the init-sw.js in our public directory.

if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
// the sw.js will be created by workbox
navigator.serviceWorker.register("/sw.js");
});
}

Also we need to add workbox to our application and have it create our service worker as already mentioned the prior code snippet.

The service worker will simply cache our resources like html pages to speed up the page.

Stale-while-revalidate Cache Strategy — Image courtesy of Workbox
$ npm install --save-dev workbox-cli
# or
$ yarn add -D workbox-cli

Now we need to create the config for workbox, stored in the root directory called workbox-config.js.

module.exports = {
globDirectory: "dist/",
globPatterns: ["**/*.{html,json,js,txt,xml,css}"],
swDest: "dist/sw.js",
};

And finally we need to update our build target in the package.json:

    //...
// replace
"build": "react-static build"
// with
"build": "react-static build && workbox generateSW"
//...

If you now build your application with...

$ npm run build
# or
$ yarn build

...it will generate a service-worker file (sw.js) in your dist folder.

Eye Candy

Up until ow, our App even didn’t had a favicon, if our already got an Icon or Idea for an Icon, that is great.

Unfortunately we need to prepare our raw icon to be used for certain Operating Systems like Android and make the maskable. There is a great tool around which is called maskable.app with editing capabilities located at Maskable.app Editor. There you can upload your icon and make it fit your needs.

Maskable.app Editor in action

Now we have our saanitized icon, which we want to have for the variety of different OS/Browser preferences. I used the pwa-asset-generator which is well introduced in the article PWA splash screen and icon generator.

In our public directory, we create an icons folder containing our maskable icon which I’ve named app_icon.png.

$ cd public
$ npx pwa-asset-generator ./icons/app_icon.png ./icons --background "#fff" --scrape false
getSplashScreenMetaData Skipped scraping - using static data
saveImages Initialising puppeteer to take screenshots 🤖
getShellHtml Generating shell html with provided image source
getShellHtml Providing shell html as page content
saveImages Saved image apple-splash-2048-2732 🙌
saveImages Saved image apple-splash-2160-1620 🙌
saveImages Saved image apple-splash-1792-828 🙌
saveImages Saved image apple-splash-828-1792 🙌
saveImages Saved image apple-splash-1284-2778 🙌
saveImages Saved image apple-splash-2732-2048 🙌
saveImages Saved image apple-splash-1668-2388 🙌
saveImages Saved image apple-splash-1242-2208 🙌
saveImages Saved image apple-splash-2532-1170 🙌
saveImages Saved image apple-splash-750-1334 🙌
saveImages Saved image apple-splash-2224-1668 🙌
saveImages Saved image manifest-icon-192 🙌
saveImages Saved image apple-splash-2688-1242 🙌
saveImages Saved image apple-splash-1334-750 🙌
saveImages Saved image apple-splash-1620-2160 🙌
saveImages Saved image apple-splash-1536-2048 🙌
saveImages Saved image manifest-icon-512 🙌
saveImages Saved image apple-splash-1242-2688 🙌
saveImages Saved image apple-icon-180 🙌
saveImages Saved image apple-splash-1668-2224 🙌
saveImages Saved image apple-splash-1170-2532 🙌
saveImages Saved image apple-splash-1136-640 🙌
saveImages Saved image apple-splash-640-1136 🙌
saveImages Saved image apple-splash-2778-1284 🙌
saveImages Saved image apple-splash-2048-1536 🙌
saveImages Saved image apple-splash-2208-1242 🙌
saveImages Saved image apple-splash-2436-1125 🙌
saveImages Saved image apple-splash-1125-2436 🙌
saveImages Saved image apple-splash-2388-1668 🙌
cli Web App Manifest file is not specified, printing out the content to console instead 🤔
cli Below is the icons content for your manifest.json file. You can copy/paste it manually 🙌
[
{
"src": "/icons/manifest-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/manifest-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
cli Index html file is not specified, printing out the content to console instead 🤔
cli Below is the iOS meta tags content for your index.html file. You can copy/paste it manually 🙌
<link rel="apple-touch-icon" href="/icons/apple-icon-180.png" /><meta name="apple-mobile-web-app-capable" content="yes" /><link rel="apple-touch-startup-image" href="/icons/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="/icons/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" />

As we can see, it successfully generated our icons in a variety of sizes as well as generated the code to be inserted in the manifest.json and in our case App.js. You can checkout the code directly on Github containing all the necessary changes.

Code

The Code can be found at Github https://github.com/chrkaatz/static-app-tutorial where each tutorial step will have it’s respective commit and tag associated with it. This one is tagged with 03-React-Static-PWA.

Tutorial

  1. Part I — Bootstrap your App with react-static and grommet
  2. Part II — React-static app deployment and CI
  3. Part III — React-static PWA
  4. Part IV — React-static app testing with Cypress

--

--

Dedicated Solution Engineer with focus on agile software development, team culture and evolutionary architectures.