A Progressive Web Application (PWA) is a type of web app that runs in the browser but can also be installed on mobile devices. These apps are built using standard web technologies such as JavaScript, HTML, and CSS, with the goal of creating an application that works across different platforms and standards-compliant browsers, including desktop computers and smartphones.
What Is a Progressive Web App (PWA)?
A PWA is not a new framework or a special JavaScript library. Instead, it’s a set of best practices and browser features that make a web app feel like a native mobile app—the kind we usually install from the Google Play Store (Android) or the App Store (Apple devices). The goal is to offer web users an experience similar to native applications: fast, reliable, installable, and engaging.
A Progressive Web App should work well on all kinds of devices with different capabilities and hardware limits. Some advanced features may not be available on very old devices, but the app itself should still run smoothly, especially in terms of performance and speed. Because of these advantages, if you own a Blogger blog or a website, it’s highly recommended to add a PWA layer to your site.
Steps to Create a PWA for Blogger
To create a Progressive Web App, you need to add several features to your website. These allow your site to work offline, send notifications (depending on your implementation), and be installable on the user’s home screen via an “Add to Home Screen” prompt on mobile or desktop.
Requirements
Before we start, make sure you have the following ready to activate PWA on your Blogger site:
- Prepare your blog logo as a 512 × 512 PNG image.
- You must have a GitHub account.
- Your blog should be using Cloudflare for DNS / proxy management.
Generate Your Icons
- Prepare a PNG logo for your blog at 512 × 512.
- Rename the file to android-icon-512x512.png.
- Go to favicon-generator.org and upload your blog icon.
- Download the generated favicon package and extract all files.
- Delete unnecessary files such as:
browserconfig.xmlmanifest.json
Also Read:
Upload Icons to GitHub
- Create a new repository on GitHub.com, for example: icon-medoapp.
- Upload all the favicon/icon files to the main branch.
- Also upload your original logo android-icon-512x512.png into the same repo.
- The total number of icon files will be around 26 in most cases.
Create Cloudflare Workers
- 1 – Log in to your account at Cloudflare.com.
- 2 – Go to the Workers section and click Manage Workers.
-
3 – Click Create Service and name the service
main-blogname, for examplemain-medoapp. Then delete the default script and replace it with the following:
addEventListener("fetch", event => {
event.respondWith(handleRequest(event))
})
//const BUCKET_NAME = "main"
const BUCKET_URL = `https://cdn.statically.io/gh/Mr-medo-net/app-icon`
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 })
}
}
Replace the highlighted parts (like Mr-medo-net/app-icon) with your own GitHub username and repository path.
Then click Save and Deploy.
Manifest.json Worker
In the same Workers section, create another service and name it
manifest-blogname, e.g. manifest-medoapp.
Replace its script with the following:
addEventListener("fetch", event => {
const data = {
name: "Master Medo Website",
short_name: "Master Medo App",
description: "Install our website app and browse the latest posts even when you are offline. Install the app now from TechnoPraveen.com.",
display: "standalone",
prefer_related_applications: false,
start_url: "\/?utm_source=homescreen",
scope: "\/",
background_color: "#BA0000",
theme_color: "#BA0000",
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: "Home Page",
short_name: "Homepage",
description: "Master Medo | Tutorials and digital tech guides.",
url: "\/?utm_source=homescreen",
icons: [
{
src: "\/main\/home-icon-192x192.png",
sizes: "192x192"
}
]
},
{
name: "Latest Posts",
short_name: "Blog",
description: "Browse the latest posts.",
url: "\/search?utm_source=homescreen",
icons: [
{
src: "\/main\/trend-icon-192x192.png",
sizes: "192x192"
}
]
},
{
name: "Apps & Games",
short_name: "Apps & Games",
description: "All apps and games from our site.",
url: "\/search\/label\/تطبيقات?utm_source=homescreen",
icons: [
{
src: "\/main\/appsm-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"
}
})
)
})
Replace any parts you want (name, description, color codes, paths, etc.) with your own values, then click Save and Deploy.
Service Worker (Cloudflare Worker)
As mentioned above, create another service and name it
serviceworker-blogname, for example serviceworker-medoapp.
Replace its code with the following:
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);
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))
})
Here we use Workbox to temporarily cache HTML, CSS, JS, and other static assets. Click Save and Deploy after editing if needed.
Offline Page Worker
Now create another service and name it
offline-blogname, e.g. offline-medoapp, and replace its code with:
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='#BA0000' name='theme-color'/>
<meta content='#BA0000' name='msapplication-navbutton-color'/>
<meta content='#BA0000' 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 there is a problem with your internet connection.</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))
})
You can customize any highlighted parts (texts, colors, icons) as you like. Then click Save and Deploy.
Create Routes for the Workers
Now go to the Workers section in Cloudflare and click Add Route. Fill the fields similar to the table below (replace the domain and names with your own):
| Route | Service | Environment |
|---|---|---|
| www.TechnoPraveen.com/main/* | main-medoapp | production |
| www.TechnoPraveen.com/manifest.json | manifest-medoapp | production |
| www.TechnoPraveen.com/sw.js | serviceworker-medoapp | production |
| www.TechnoPraveen.com/offline.html | offline-medoapp | production |
Enter the routes using your own domain name instead of www.TechnoPraveen.com.
Edit Your Blogger Template
Now go to your Blogger Dashboard, open your blog, then click Theme → Edit HTML.
Paste the following HTML links and meta tags right below the
<head> tag (or <head> if it is escaped in your template):
<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='#BA0000' name='msapplication-TileColor'/>
<meta content='/main/ms-icon-144x144.png' name='msapplication-TileImage'/>
<meta content='#BA0000' name='theme-color'/>
<link href='/manifest.json' rel='manifest'/>
Next, scroll down to the bottom of the template and add the following JavaScript code
right above the closing </body> tag:
<script>/*<![CDATA[*/
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
deferredPrompt = e;
});
const installApp = document.getElementById('installApp');
if (installApp) {
installApp.addEventListener('click', async () => {
if (deferredPrompt !== null) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
deferredPrompt = null;
}
}
});
}
/* Service Worker */
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('ServiceWorker registration successful');
}).catch(registrationError => {
console.log('ServiceWorker registration failed: ', registrationError);
});
});
};
/*]]>*/</script>
Finally, you need a button in your layout or posts that will trigger the installation prompt when the PWA is available. You can place this anywhere you like:
<button id="installApp">Install</button>
That’s it! Now your Blogger site has a Progressive Web App layer: icons, manifest, service worker, offline page, and an install button. If you face any problems or have questions, feel free to ask in the comments section of your blog.
This concludes our tutorial on "How to Create a Progressive Web App (PWA) for Blogger". See you again in a new article on Mr Medo Blog. Stay safe! :)