How to build a PWA (Progressive Web App) for Blogger

This post is outdated, a new post on this topic will be published soon. Stay connected with Fineshop Design!
What's updated?
1. You no longer need to create multiple Workers app which means only 1 route.
2. Easy to update the files.
3. Uses ES Modules format for Workers.

Looks like Statically CDN is not working properly (reported by blog post readers). Please wait until I update this post.

Hello! Welcome to Fineshop Design.

Have you ever noticed on some website when you visit, it shows an "Add To Home Screen" prompt? If yes and you also want to create for your website then you are at the right place.

Activate Progressive Web App in Blogger
© Fineshop Design | Progressive Web App for Blogger

In this article, I will guide you to build a PWA (Progressive Web app) for your website in an easy way.

So, before you start the process to set up PWA For your website, you must know what is PWA and why it is important for your website.

What is PWA (Progressive Web App)

Progressive web apps are a new way to create native-like experiences on the web. They combine the best of both worlds, providing users with the best of a website and an app.

A Progressive Web App is a webview app that can be installed on your phone or tablet like an app, but it's built with web technologies. This means you can add features like push notifications and offline support without having to build a separate native app.

Why is PWA Important?

A Progressive Web App is a website that behaves like an app on the user’s device. It loads quickly, is responsive to different devices, and can be accessed at any time without the need to download anything.

A Progressive Web App offers a better experience than a traditional website for both users and developers. It has features that are usually found in native apps such as push notifications, offline support, and home screen icons. This means that it will load faster, look better on all types of screens, have more functionality than a regular website, and will be available even when the device is offline.

How to build a PWA for Blogger

In order to build a Progressive Web App, you will need to add some features to your website. These features include service workers, which allow your site to work offline, and push notifications for when users return to your site. You can also install an Add-to-Home screen prompt on your website that prompts users to add your site or app to their home screen on their mobile device or desktop computer.

This tutorial may be a bit difficult to understand, but if you follow all the steps correctly, you will surely be able to build a PWA for your Blogger Website.

This process requires a Custom Domain with the integration of Cloudflare, and it can't be done on the .blogspot subdomain with this process. With .blogspot you can't set up service worker.

Requirements

Before we start, there are several things which must be required for Activating PWA:

  1. A Blog Icon in .png extension with a size of 512×512
  2. 5 Screenshots of your Web Pages in .png extension
  3. Must have a GitHub Account
  4. DNS Management: Blog must pass Cloudflare

Uploading Icons

  1. Prepare an icon for your blog
  2. in .png extension with a size of 512×512.
    *Rename the file as android-icon-512x512.png
  3. Go to favicon-generator.org and upload the Blog Icon.
  4. Download the generated favicon and extract the files.
  5. *Delete unnecessary files like:
    browserconfig.xml
    manifest.json
  6. Create a Repository, i.e. icon-fineshop on GitHub and upload all the icon files in main branch.
    *Upload the original file as well, i.e. android-icon-512x512.png.
    Total number of icons will be approximately 26.

Uploading Screenshots

  1. Prepare 5 screenshots of your Web Pages in .png extension that will appear when it shows the install button.
  2. Name the screenshots in series:
    scr1.png
    scr2.png
    scr3.png
    scr4.png
    scr5.png
  3. Upload these screenshots in the main branch of the same GitHub Repository.

Creating Workers in Cloudflare

We will create 4 Workers in Cloudflare, to make it easier to create a ROUTE later, I will suggest you to save it with the worker name and blog name, for example:
main-fineshop
manifest-fineshop
serviceworker-fineshop
offline-fineshop

Main Worker

  1. Login to your Cloudflare Account.
  2. Go to Workers section and click on Manage Workers.
  3. Click on Create a Service and rename the service as main-blogname, i.e. main-fineshop.
  4. Delete the existing script, replace it with the following script:
addEventListener("fetch", event => {
  event.respondWith(handleRequest(event))
})

//const BUCKET_NAME = "main"
const BUCKET_URL = `https://cdn.statically.io/gh/kumardeo080/icon-fineshop`

async function serveAsset(event) {
  const url = new URL(event.request.url)
  const cache = caches.default
  let response = await cache.match(event.request)

  if (!response) {
    response = await fetch(`${BUCKET_URL}${url.pathname}`)
    const headers = { "cache-control": "public, max-age=14400" }
    response = new Response(response.body, { ...response, headers })
    event.waitUntil(cache.put(event.request, response.clone()))
  }
  return response
}

async function handleRequest(event) {
  if (event.request.method === "GET") {
    let response = await serveAsset(event)
    if (response.status > 399) {
      response = new Response(response.statusText, { status: response.status })
    }
    return response
  } else {
    return new Response("Method not allowed", { status: 405 })
  }
}
  1. *Replace the marked parts, i.e.
    kumardeo080 with your GitHub username
    icon-fineshop with your Repository name
  2. Click on Save and Deploy.

Manifest.json

  1. In the same way, create a Service and rename as manifest-blogname, i.e. manifest-fineshop.
  2. Replace the existing script with the following script:
addEventListener("fetch", event => {
  const data = {
    name: "Fineshop Design",
    short_name: "Fineshop Design",
    description: "Install Now Fineshop Design - Let's fuel creativity",
    display: "standalone",
    prefer_related_applications: false,
    start_url: "\/?utm_source=homescreen",
    scope: "\/",
    background_color: "#2196f3",
    theme_color: "#2196f3",
    icons: [
      {
      src: "\/main\/android-icon-512x512.png",
      sizes: "512x512",
      type: "image\/png",
      density: "4.0",
      purpose: "any maskable"
      },
      {
      src: "\/main\/android-icon-192x192.png",
      sizes: "192x192",
      type: "image\/png",
      density: "4.0",
      purpose: "any maskable"
      },
      {
      src: "\/main\/apple-icon-144x144.png",
      sizes: "144x144",
      type: "image\/png",
      density: "3.0",
      purpose: "any maskable"
      },
      {
      src: "\/main\/android-icon-96x96.png",
      sizes: "96x96",
      type: "image\/png",
      density: "2.0",
      purpose: "any maskable"
      },
      {
      src: "\/main\/android-icon-72x72.png",
      sizes: "72x72",
      type: "image\/png",
      density: "1.5",
      purpose: "any maskable"
      },
      {
      src: "\/main\/android-icon-48x48.png",
      sizes: "48x48",
      type: "image\/png",
      density: "1.0",
      purpose: "any maskable"
      },
      {
      src: "\/main\/android-icon-36x36.png",
      sizes: "36x36",
      type: "image\/png",
      density: "0.75",
      purpose: "any maskable"
      }
    ],
    shortcuts: [
      {
      name: "Fineshop Design",
      short_name: "Fineshop Design",
      description: "The Best Website where you can find Blogger Widgets, Tech News, Tech Reviews, Coding related Tutorials and many more.",
      url: "\/?utm_source=homescreen",
      icons: [
          {
          src: "\/main\/android-icon-192x192.png",
          sizes: "192x192"
          }
        ]
      },
      {
      name: "Fineshop Design - Blog",
      short_name: "Fineshop Design - Blog",
      description: "Explore Fineshop Design Blog.",
      url: "\/search?utm_source=homescreen",
      icons: [
          {
          src: "\/main\/android-icon-192x192.png",
          sizes: "192x192"
          }
        ]
      },
      {
      name: "Blogger Widgets",
      short_name: "Blogger Widgets",
      description: "Useful Widgets for your Blog.",
      url: "\/search\/label\/Widgets?utm_source=homescreen",
      icons: [
          {
          src: "\/main\/android-icon-192x192.png",
          sizes: "192x192"
          }
        ]
      }
    ],
    screenshots: [
      {
      src: "\/main\/scr1.png",
      type: "image\/png",
      sizes: "540x720"
      },
      {
      src: "\/main\/scr2.png",
      type: "image\/png",
      sizes: "540x720"
      },
      {
      src: "\/main\/scr3.png",
      type: "image\/png",
      sizes: "540x720"
      },
      {
      src: "\/main\/scr4.png",
      type: "image\/png",
      sizes: "540x720"
      },
      {
      src: "\/main\/scr5.png",
      type: "image\/png",
      sizes: "540x720"
      }
    ],
    serviceworker: {
      src: "\/sw.js"
    }
  }

  const json = JSON.stringify(data, null, 2)

  return event.respondWith(
    new Response(json, {
      headers: {
        "content-type": "application/json;charset=UTF-8"
      }
    })
  )
})
  1. *Replace the marked parts as per your need.
    Also note down the color code.
  2. Click on Save and Deploy.

ServiceWorker

  1. As above, create a Service and rename as serviceworker-blogname, i.e. serviceworker-fineshop.
  2. Replace the existing script with the following script:
const js = `
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');
if (workbox) {
workbox.core.skipWaiting();
workbox.core.clientsClaim();
workbox.core.setCacheNameDetails({
  prefix: 'thn-sw',
  suffix: 'v22',
  precache: 'install-time',
  runtime: 'run-time'
});

const FALLBACK_HTML_URL = '/offline.html';
const version = workbox.core.cacheNames.suffix;
workbox.precaching.precacheAndRoute([{url: FALLBACK_HTML_URL, revision: null},{url: '/manifest.json', revision: null},{url: '/main/favicon.ico', revision: null}]);

workbox.routing.setDefaultHandler(new workbox.strategies.NetworkOnly());

workbox.routing.registerRoute(
    new RegExp('.(?:css|js|png|gif|jpg|svg|ico)$'),
    new workbox.strategies.CacheFirst({
        cacheName: 'images-js-css-' + version,
        plugins: [
            new workbox.expiration.ExpirationPlugin({
                maxAgeSeconds: 60 * 24 * 60 * 60,
                maxEntries:200,
                purgeOnQuotaError: true
            })
        ],
    }),'GET'
);

workbox.routing.setCatchHandler(({event}) => {
      switch (event.request.destination) {
        case 'document':
        return caches.match(FALLBACK_HTML_URL);
      break;
      default:
        return Response.error();
  }
});

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches
      .keys()
      .then(keys => keys.filter(key => !key.endsWith(version)))
      .then(keys => Promise.all(keys.map(key => caches.delete(key))))
  );
});

}
else {
    console.log('Oops! Workbox did not load');
}
`

async function handleRequest(request) {
  return new Response(js, {
    headers: {
      "content-type": "application/javascript;charset=UTF-8",
    },
  })
}

addEventListener("fetch", event => {
  return event.respondWith(handleRequest(event.request))
})
  1. We have used Workbox, so we can cache the HTML, CSS, JS, and any static files.
  2. Click on Save and Deploy.

Offline

  1. As above, create a Service and rename as offline-blogname, i.e. offline-fineshop.
  2. Replace the existing script with the following script:
const html = `<!DOCTYPE html>
<html>

<head>
  <!--[ Meta Tags ]-->
  <title>Oops, You're Offline!</title>
  <meta charset='UTF-8'/>
  <meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' name='viewport'/>
  <meta content='IE=edge' http-equiv='X-UA-Compatible'/>

  <!--[ Theme Color ]-->
  <meta content='#2196f3' name='theme-color'/>
  <meta content='#2196f3' name='msapplication-navbutton-color'/>
  <meta content='#2196f3' name='apple-mobile-web-app-status-bar-style'/>
  <meta content='true' name='apple-mobile-web-app-capable'/>

  <!--[ Favicon ]-->
  <link href='/main/apple-icon-120x120.png' rel='apple-touch-icon' sizes='120x120'/>
  <link href='/main/apple-icon-152x152.png' rel='apple-touch-icon' sizes='152x152'/>
  <link href='/main/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png'/>
  <link href='/main/favicon-96x96.png' rel='icon' sizes='96x96' type='image/png'/>
  <link href='/main/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png'/>
  <link href='/main/favicon.ico' rel='icon' type='image/x-icon'/>
  <link href='/main/favicon.ico' rel='shortcut icon' type='image/x-icon'/>

  <!--[ Stylesheet ]-->
  <style>/*<![CDATA[*/
/* Merriweather - Font */ @font-face{font-family: 'Merriweather'; font-style: italic; font-weight: 300; font-display: swap; src: local('Merriweather-LightItalic'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR7lXff4jvw.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR7lXcf8.woff) format('woff')} @font-face{font-family: 'Merriweather'; font-style: italic; font-weight: 700; font-display: swap; src: url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR7NWPf4jvw.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR71Wsf8.woff) format('woff')} @font-face{font-family: 'Merriweather'; font-style: italic; font-weight: 900; font-display: swap; src: url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR7NWPf4jvw.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4l0qyriQwlOrhSvowK_l5-eR7NWMf8.woff) format('woff')} @font-face{font-family: 'Merriweather'; font-style: normal; font-weight: 300; font-display: swap; src: url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l521wRZWMf6.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l521wRpXA.woff) format('woff')} @font-face{font-family: 'Merriweather'; font-style: normal; font-weight: 700; font-display: swap; src: url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l52xwNZWMf6.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l52xwNpXA.woff) format('woff')} @font-face{font-family: 'Merriweather'; font-style: normal; font-weight: 900; font-display: swap; src: url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l52_wFZWMf6.woff2) format('woff2'), url(https://fonts.gstatic.com/s/merriweather/v22/u-4n0qyriQwlOrhSvowK_l52_wFpXA.woff) format('woff')}
/* Content */ body{background:#f1f3f6;color:#1f1f1f;font-family:'Merriweather',serif;font-weight:400;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}body:focus{outline:none !important} .mainCont{margin:0 auto;position:fixed;left:0;top:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;padding:15px} .noIntPop{position:relative;overflow:hidden;text-align:center;padding:15px;border-radius:30px;background:#f1f3f6;box-shadow:inset 0 0 15px rgba(55, 84, 170, 0), inset 0 0 20px rgba(255, 255, 255, 0), 7px 7px 15px rgba(55, 84, 170, 0.15), -7px -7px 20px white, inset 0px 0px 4px rgba(255, 255, 255, 0.2)} .circle.t{top:-150px;right:-150px} .circle.b{bottom:-150px;left:-150px} .noIntCont{position:relative;z-index:1} .noIntIcon{padding:30px} .noConHead{font-weight:700;font-size:1.3rem} .noConDesc{font-size:16px;line-height:1.4em;padding-top:20px;font-weight:400;opacity:.8} .cta,.relCont{display:flex;justify-content:center;align-items:center} .relCont{padding:30px} .cta{width:66px;height:66px;background:#f1f3f6;outline:none;border:none;border-radius:690px;box-shadow:inset 0 0 15px rgba(55, 84, 170, 0), inset 0 0 20px rgba(255, 255, 255, 0), 7px 7px 15px rgba(55, 84, 170, 0.15), -7px -7px 20px white, inset 0px 0px 4px rgba(255, 255, 255, 0.2);transition:box-shadow 399ms ease-in-out} .cta:hover{box-shadow:inset 7px 7px 15px rgba(55, 84, 170, 0.15), inset -7px -7px 20px white, 0px 0px 4px rgba(255, 255, 255, 0.2)} .icon{content:'';width:25px;height:25px;display:inline-block} .iconB{content:'';width:50px;height:50px;display:inline-block} .icon.reload{background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239dabc0' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='23 4 23 10 17 10'/><path d='M20.49 15a9 9 0 1 1-2.12-9.36L23 10'/></svg>") center / 25px no-repeat} .iconB.wifiOff{background:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231f1f1f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='1' y1='1' x2='23' y2='23'/><path d='M16.72 11.06A10.94 10.94 0 0 1 19 12.55'/><path d='M5 12.55a10.94 10.94 0 0 1 5.17-2.39'/><path d='M10.71 5.05A16 16 0 0 1 22.58 9'/><path d='M1.42 9a15.91 15.91 0 0 1 4.7-2.88'/><path d='M8.53 16.11a6 6 0 0 1 6.95 0'/><line x1='12' y1='20' x2='12.01' y2='20'/></svg>") center / 50px no-repeat} .circle{position:absolute;z-index:1;width:280px;height:280px;border-radius:50%;background-color:#f1f3f6;box-shadow:inset 8px 8px 12px #d1d9e6, inset -8px -8px 12px #f9f9f9}
  /*]]>*/</style>
</head>

<body>
  <div class='mainCont notranslate'>
    <div class='noIntPop'>
      <div class='circle t'></div>
      <div class='circle b'></div>
      <div class='noIntCont'>
        <div class='noIntIcon'>
          <i class='iconB wifiOff'></i>
        </div>
        <div class='noConHead'>Oops, You're Offline!</div>
        <div class='noConDesc'>It looks like your network connection isn't working right now.</div>

        <div class='relCont'>
          <button class='cta' onclick='window.location.reload()'>
            <i class='icon reload'></i>
          </button>
        </div>
      </div>
    </div>
  </div>
</body>

</html>`

async function handleRequest(request) {
  return new Response(html, {
    headers: {
      "content-type": "text/html;charset=UTF-8",
    },
  })
}

addEventListener("fetch", event => {
  return event.respondWith(handleRequest(event.request))
})
  1. *Replace the marked parts as per your wish.
  2. Click on Save and Deploy.

Creating Routes

  1. Now go back to Workers section and then click on Add Route.
  2. Input the fields as shown in the given table:
Route Service Environment
www.fineshopdesign.com/main/* main-fineshop production
www.fineshopdesign.com/manifest.json manifest-fineshop production
www.fineshopdesign.com/sw.js serviceworker-fineshop production
www.fineshopdesign.com/offline.html offline-fineshop production
  1. *Input the fields as per your blog url and blog name.

Now try to open URLs one by one, i.e
www.fineshopdesign.com/main/android-icon-512x512.png
www.fineshopdesign.com/manifest.json
www.fineshopdesign.com/sw.js
www.fineshopdesign.com/offline.html
If you are able to open these URLs that means there is no problem, you have done with the Cloudflare setups.
Now it's time to make changes in blog.

Editing Blog

  1. Now go to Blogger Dashboard. Go to Theme section.
  2. Click on Edit HTML.
  3. Paste the following codes below <head>, if you didn't find it, it would have been probably parsed which is &lt;head&gt;. Delete existing similar codes.
<link href='/main/apple-icon-57x57.png' rel='apple-touch-icon' sizes='57x57'/>
<link href='/main/apple-icon-60x60.png' rel='apple-touch-icon' sizes='60x60'/>
<link href='/main/apple-icon-72x72.png' rel='apple-touch-icon' sizes='72x72'/>
<link href='/main/apple-icon-76x76.png' rel='apple-touch-icon' sizes='76x76'/>
<link href='/main/apple-icon-114x114.png' rel='apple-touch-icon' sizes='114x114'/>
<link href='/main/apple-icon-120x120.png' rel='apple-touch-icon' sizes='120x120'/>
<link href='/main/apple-icon-114x114.png' rel='apple-touch-icon' sizes='144x144'/>
<link href='/main/apple-icon-152x152.png' rel='apple-touch-icon' sizes='152x152'/>
<link href='/main/apple-icon-180x180.png' rel='apple-touch-icon' sizes='180x180'/>
<link href='/main/android-icon-192x192.png' rel='icon' sizes='192x192' type='image/png'/>
<link href='/main/favicon-32x32.png' rel='icon' sizes='32x32' type='image/png'/>
<link href='/main/favicon-96x96.png' rel='icon' sizes='96x96' type='image/png'/>
<link href='/main/favicon-16x16.png' rel='icon' sizes='16x16' type='image/png'/>
<link href='/main/favicon.ico' rel='icon' type='image/x-icon'/>
<meta content='#2196f3' name='msapplication-TileColor'/>
<meta content='/main/ms-icon-144x144.png' name='msapplication-TileImage'/>
<meta content='#2196f3' name='theme-color'/>
<link href='/manifest.json' rel='manifest'/>
  • Replace the color code with code you used in manifest.json

Now follow the steps as per your template, i.e. AMP Template or Non-AMP Template.

AMP Template

If you are using an AMP Template, you must follow the below steps:

  1. Add the following AMP Serviceworker JS below to <head> or above to </head>.
<script async='async' custom-element='amp-install-serviceworker' src='https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js'/>
  1. Paste the following codes above to </body>.
<amp-install-serviceworker data-iframe-src='/offline.html' layout='nodisplay' src='/sw.js'/>

Non-AMP Template

If you are using a Non-AMP Template, you must follow the below steps:

  1. Add the following Javascript code above to </body>.
<script>/*<![CDATA[*/ /* Service Worker */ if('serviceWorker' in navigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('/sw.js').then(registration=>{console.log('ServiceWorker registeration successful')}).catch(registrationError=>{console.log('ServiceWorker registration failed: ', registrationError)})})}; /*]]>*/</script>

Lastly, save the changes and visit your blog on your mobile phone, i.e. Android, you will be able to see a button to install your Progressive Web App.

Purging JsDelivr Cache

Many of you asked about purging cache of JsDelivr files.

For that simply open the url in your browser:

https://purge.jsdelivr.net/gh/<GITUSERNAME>/<REPO>@<BRANCH>/<PATH>

Conclusion

This is all about building a PWA (Progressive Web App) for your Blogger Website. I hope you enjoy this article. Please do share this article. And if you are facing problem in any section or you have any question then ask us in . Thank you!