Gatsby blog as PWA (Progressive Web App)

November 26, 2022

Starting with some of the benefits, installed PWAs can bring more user engagement and conversions. On the user side, it brings the possibility to read posts offline. Progressive Web App 101 post covers more details about PWAs.


  • bootstrapped Gatsby blog
  • installed manifest (gatsby-plugin-manifest) and offline (gatsby-plugin-offline) plugins


Add plugin configurations to the Gatsby configuration file. The manifest plugin should be loaded before the offline plugin.

Prepare the app icon in 512x512 pixels, and the manifest plugin will generate the icons in all the necessary dimensions. PWA usage can be logged with the UTM link in start_url property.

Runtime caching for static resources (JavaScript, CSS, and page data JSON files) is set to network-first caching, so it retrieves the latest changes before showing them to the user. In case of issues with caching in a local environment, an offline plugin can be disabled.

// gatsby-config.js
const plugins = [
// ...
resolve: `gatsby-plugin-manifest`,
options: {
name: `app name`,
short_name: `app name`,
start_url: `/?utm_source=pwa&utm_medium=pwa&utm_campaign=pwa`,
background_color: `#FFF`,
theme_color: `#2F3C7E`,
display: `standalone`,
icon: `src/assets/icon.png`
if (process.env.NODE_ENV !== 'development') {
resolve: `gatsby-plugin-offline`,
options: {
workboxConfig: {
runtimeCaching: [
urlPattern: /(\.js$|\.css$|static\/)/,
handler: `NetworkFirst`
urlPattern: /^https?:.*\/page-data\/.*\.json/,
handler: `NetworkFirst`
urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
handler: `StaleWhileRevalidate`
urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
handler: `StaleWhileRevalidate`
module.exports = {
// ...

Service worker updates can also be detected. For a better user experience, a user should approve refreshing the page before updating it to the latest version.

// gatsby-browser.js
exports.onServiceWorkerUpdateReady = () => {
const shouldReload = window.confirm(
'This website has been updated. Reload to display the latest version?'
if (shouldReload) {
window.location.href = window.location.href.replace(/#.*$/, '');
exports.onRouteUpdate = async () => {
if (!navigator) {
console.log('Navigator is not defined, skipping service worker registration...');
if (!navigator.serviceWorker) {
console.log('Service worker is not supported, skipping registration...');
try {
const registration = await navigator.serviceWorker.register('/sw.js');
await registration.update();
} catch (error) {
console.error('Service worker registration failed', error);

Progressive Web Apps 101

March 5, 2022

Progressive Web Apps bring some advantages over native mobile apps

  • automatic updates can be implemented
  • the installed app takes less memory
  • installable on phones, tablets, desktops

Prerequisites for installation

  • web app is running over an HTTPS connection
  • service worker is registered
  • web app manifest (manifest.json) is included

Service worker

Read more about it on Caching with service worker and Workbox


Following fields can be included

  • name is a full name used when the app is installed
  • short_name is a shorter version of the name that is shown when there is insufficient space to display the full name
  • background_color is used on a splash screen
  • description is shown on an installation pop-up
  • display customizes which browser UI is shown when the app is launched (standalone, fullscreen, minimal-ui, browser)
  • icons is a list of icons for the browser used in different places (home screen, app launcher, etc.)
  • scope specifies the navigation scope of the PWA. It should start with the URL from start_url value. If the user navigates outside the scope, PWA won't be open from external URLs
  • screenshots is a list of screenshots shown on the installation pop-up
  • start_url is a relative URL of the app which is loaded when the installed app is launched. PWA usage can be tracked by adding UTM parameters within the URL.
  • theme_color sets the color of the toolbar, it should match the meta theme color specified in the document head

Description and screenshots are shown only on mobile phones.

"name": "App name",
"short_name": "App short name",
"background_color": "#ffffff",
"description": "App description",
"display": "standalone",
"icons": [
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
"scope": "/app",
"screenshots": [{
"src": "screenshots/main.jpg",
"sizes": "1080x2400",
"type": "image/jpg"
"start_url": "/app?utm_source=pwa&utm_medium=pwa&utm_campaign=pwa",
"theme_color": "#3366cc"

Manifest file should be included via link tag

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

In-app installation experience

It can be implemented on Google Chrome and Edge.

  • listen for the beforeinstallprompt event
  • save beforeinstallprompt event so it can be used to trigger the installation
  • provide a button to start the in-app installation flow
let deferredPrompt;
let installable = false;
window.addEventListener("beforeinstallprompt", (event) => {
deferredPrompt = event;
installable = true;
document.getElementById("installable-btn").innerHTML = "Install";
window.addEventListener("appinstalled", () => {
installable = false;
document.getElementById("installable-btn").addEventListener("click", () => {
if (installable) {
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
document.getElementById("installable-btn").innerHTML = "click!";
} else {


chrome://webapks page on mobile phones shows the list of installed PWAs with their details. Last Update Check Time is useful for checking when the manifest file was updated. The app is updated once a day if there are some manifest changes.


The demo with the mentioned examples is available here.


Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.