Have you ever noticed on some website when you visit, it shows an "Install" prompt? If yes and you also want to create for your website then you are at the right place.
You can also Submit Your PWA to our PWA Store
In this article, wel will learn how to build a PWA (Progressive Web App) for your website in an easy way.
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:- A Blog Icon in .png extension with a size of 512×512
- 5 Screenshots of your Web Pages in .png extension ( For screenshots use Progressier)
- Must have a GitHub Account
- DNS Management: Blog must pass Cloudflare
Uploading Icons
- Prepare an icon for your blog in .png extension with a size of 512×512. *Rename the file as android-icon-512x512.png
- Go to favicon-generator.org and upload the Blog Icon.
- Download the generated favicon and extract the files. *Delete unnecessary files like: browserconfig.xml manifest.json
- 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.
Creating Workers in Github and Cloudflare
Manifest
- Go to your Github repository.
- Click on add file, then create file. Here is an example screenshot.
Manifest.JSON
- Double click to copy following.
- Paste it in new file created on github repository as taught before with screenshot example.
- Change all values to as per your website.
- Go to your github repository on new tab again.
- Use jsdeliver to get your all icons from repository to replace following icon files in following manifest.
- https://cdn.jsdelivr.net/gh/(github username)/repository/ icon file.
- Paste it in browser address-bar to see if your icon is being shown.
- Replace all icons, give correct sizes. Your manifest file is ready.
- Now name your file as Manifest.JSON and click on Commit Changes. like shown in screenshot below.
{ "name": "Class With Mason", "short_name": "Class w Mason", "description": "Install Now and Fuel the Creativity", "lang": "en-US", "display": "standalone", "background_color": "#ffffff", "start_url": "https://www.classwithmason.com", "theme_color": "#ffffff", "orientation": "portrait", "dir": "ltr", "prefer_related_applications": false, "icons": [ { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/favicon-32x32.png", "sizes": "32x32", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/apple-icon-72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/favicon-96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/apple-icon-120x120.png", "sizes": "120x120", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/android-icon-144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/apple-icon-152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm@main/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm/an-android-icon-512x512.png", "sizes": "512x512", "type": "image/png" } ], "screenshots": [ { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm/screenshot1%20(1).jpg", "sizes": "1080x1920", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm/screenshot1%20(2).jpg", "sizes": "1080x1920", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm/screenshot1%20(3).jpg", "sizes": "1080x1920", "type": "image/png" }, { "src": "https://cdn.jsdelivr.net/gh/Mushahid7734/icon-cwm/screenshot%20big.png", "sizes": "1280x800", "type": "image/png" } ], "serviceworker": { "src": "https://classwithmason.com/sw.js" } }
Adding your Manifest.JSON to blogger blog template.
- Just like we used jsdelivr for icons, replace github username with your username, your repository, and manifest.json. Because careful about capitalisation, if it is Manifest with capital M or otherwise then place it exactly so in jsdelivr address. Try it by placing it in your browser address bar and clicking enter. If same manifest is shown then you are done.
- Go to your blogger account. Click on Theme. On customise, edit HTML. then paste this jsdelivr address with your manifest.JSON just below the "head" section of your template.
Service Worker
- Login to your Cloudflare Account.
- Go to Workers section and click on Manage Workers.
- Click on Create Application/worker and rename it as sw.js
- Delete the existing script, replace it with the following script:
- Replace manifest.json address with yours through jsdelivr.
- replace favicon.ico address with jsdelivr address for it.
- keep /offline.html for offline page the same.
- Save it
const js = ` importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/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: 'https://cdn.jsdelivr.net/gh/(github username)/repository/manifest.JSON', revision: null},{url: '/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)) })
Adding Route
- Click on your service worker in cloudflare.
- Go to Triggers Tab.
- Click "Add Route"
- https://Your Domain/sw.js
- Disable the existing route.
Registering Service Worker
If your template is not Amp version then copy and paste following in your blogger blog template just before </body><script>/*<![CDATA[*/ /* Service Worker */ if('serviceWorker' in navigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('https://www.classwithmason.com/sw.js').then(registration=>{console.log('ServiceWorker registeration successful')}).catch(registrationError=>{console.log('ServiceWorker registration failed: ', registrationError)})})}; /*]]>*/</script>
If you are using Amp template then
1. Add the following AMP Serviceworker JS below <head> or just above </head>.
<script async='async' custom-element='amp-install-serviceworker' src='https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js'/>
2.Paste the following codes above to </body>
<amp-install-serviceworker data-iframe-src='/offline.html' layout='nodisplay' src='/sw.js'/>
Install Button
Place following css style code for install button just above ]]></b:skin>#dev_blogger{margin:10px;text-align:center;color:#000;background-color:#fff;border:none;padding:10px 20px;cursor:pointer;font-size:14px;border-radius:20px;box-shadow:0 2px 4px rgba(0,0,0,.1);}#dev_blogger:hover{background-color:#f5f5f5;}@media screen and (max-width:767px){#dev_blogger{width:100%;}}
<script>/*<![CDATA[*/ const installButton = document.getElementById("dev_blogger");window.addEventListener("beforeinstallprompt", e => {e.preventDefault();deferredPrompt = e;installButton.hidden = false;installButton.addEventListener("click", installApp);});function installApp() {deferredPrompt.prompt();installButton.disabled = true;deferredPrompt.userChoice.then(choiceResult => {if (choiceResult.outcome === "accepted") {installButton.hidden = true;} else {}installButton.disabled = false;deferredPrompt = null;});}window.addEventListener("appinstalled", evt => {console.log("appinstalled fired", evt);}); /*]]>*/</script>
<b:if cond='!data:view.isError and data:view.url != data:view.url params { amp: "1" }'> <script>/*<![CDATA[*/ const installButton = document.getElementById("dev_blogger");window.addEventListener("beforeinstallprompt", e => {e.preventDefault();deferredPrompt = e;installButton.hidden = false;installButton.addEventListener("click", installApp);});function installApp() {deferredPrompt.prompt();installButton.disabled = true;deferredPrompt.userChoice.then(choiceResult => {if (choiceResult.outcome === "accepted") {installButton.hidden = true;} else {}installButton.disabled = false;deferredPrompt = null;});}window.addEventListener("appinstalled", evt => {console.log("appinstalled fired", evt);}); /*]]>*/</script> </b:if>
<button hidden='' id='dev_blogger'>Install App</button>
Offline Page
- Create a Worker on cloudflare.
- Name it offline page.
- Click quick edit, delete existing code and past following code.
- Save it.
- Now, Add Route in Triggers tab. As we did before. https://yourdomain /offline.html
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)) })