Turn your website into a fast, installable app using Progressive Web App (PWA). This complete tutorial includes manifest.json, service-worker.js, offline.html, install button, and production-ready caching strategies.

✅ BLOG POST (FULL)
Why PWA?

A Progressive Web App (PWA) makes your website behave like a real mobile app. Users can:

Install it on Android/desktop

Open it in full-screen mode

Use it even when internet is off (offline mode)

Enjoy faster loading via smart caching

PWA requirements:

✅ HTTPS
✅ manifest.json
✅ service-worker.js

1) Folder Structure (Recommended)

Create these files in your site root:

/manifest.json
/service-worker.js
/offline.html
/assets/icons/icon-72.png
/assets/icons/icon-96.png
/assets/icons/icon-128.png
/assets/icons/icon-144.png
/assets/icons/icon-192.png
/assets/icons/icon-512.png

Icons should be exact sizes (important for installation).

2) FILE: manifest.json (COMPLETE)

Create: /manifest.json

{
"name": "Faulink Systems",
"short_name": "Faulink",
"description": "Smart digital solutions for schools, businesses & organizations.",
"start_url": "/index.php",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0b1220",
"theme_color": "#0066cc",
"icons": [
{ "src": "/assets/icons/icon-72.png", "sizes": "72x72", "type": "image/png" },
{ "src": "/assets/icons/icon-96.png", "sizes": "96x96", "type": "image/png" },
{ "src": "/assets/icons/icon-128.png", "sizes": "128x128","type": "image/png" },
{ "src": "/assets/icons/icon-144.png", "sizes": "144x144","type": "image/png" },
{ "src": "/assets/icons/icon-192.png", "sizes": "192x192","type": "image/png" },
{ "src": "/assets/icons/icon-512.png", "sizes": "512x512","type": "image/png" }
]
}
3) FILE: offline.html (COMPLETE)

Create: /offline.html

<!doctype html>
<html lang="sw">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Faulink - Offline</title>
<meta name="theme-color" content="#0066cc">
<style>
body{
margin:0; min-height:100vh; display:grid; place-items:center;
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
background:#0b1220; color:#fff; padding:18px;
}
.card{
width:min(560px,100%);
border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.06);
border-radius:18px;
padding:20px;
}
h1{margin:0 0 8px; font-size:1.35rem}
p{margin:0 0 10px; color:rgba(255,255,255,.85); font-weight:600}
a{
display:inline-block; margin-top:8px;
text-decoration:none; font-weight:800;
background:#00b3ff; color:#07101c;
padding:10px 14px; border-radius:999px;
}
</style>
</head>
<body>
<div class="card">
<h1>Uko Offline 👋</h1>
<p>Hakuna internet kwa sasa. Ukirudi online, app itajirefresh.</p>
<a href="/index.php">Fungua Home</a>
</div>
</body>
</html>
4) FILE: service-worker.js (PRODUCTION READY)

Create: /service-worker.js

✅ Strategy:

Pages (navigation): network-first, fallback cache index/offline

Assets: cache-first, save runtime assets

Safe caching (missing file haivunji install)

const CACHE_NAME = "faulink-v1";

const CORE_ASSETS = [
"/",
"/index.php",
"/offline.html",
"/manifest.json",
"/assets/icons/icon-72.png",
"/assets/icons/icon-96.png",
"/assets/icons/icon-128.png",
"/assets/icons/icon-144.png",
"/assets/icons/icon-192.png",
"/assets/icons/icon-512.png"
];

// Install
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => {
await Promise.all(
CORE_ASSETS.map((url) => cache.add(url).catch(() => null))
);
})
);
self.skipWaiting();
});

// Activate (cleanup old caches)
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.map((k) => (k !== CACHE_NAME ? caches.delete(k) : null)))
)
);
self.clients.claim();
});

// Fetch
self.addEventListener("fetch", (event) => {
const req = event.request;

// 1) Handle page navigation (HTML)
if (req.mode === "navigate") {
event.respondWith(
fetch(req)
.then((res) => {
const copy = res.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy));
return res;
})
.catch(async () => {
const cache = await caches.open(CACHE_NAME);
return (
(await cache.match(req)) ||
(await cache.match("/index.php")) ||
(await cache.match("/offline.html"))
);
})
);
return;
}

// 2) Handle assets (CSS/JS/IMG)
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;

return fetch(req)
.then((res) => {
// Save runtime assets for offline use
const copy = res.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy));
return res;
})
.catch(async () => {
// If offline & no cache, just return nothing (browser handles)
return cached;
});
})
);
});
5) Add PWA tags inside <head> (COMPLETE)

Add these in your index.php <head>:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#0066cc">

<link rel="icon" type="image/png" sizes="72x72" href="/assets/icons/icon-72.png">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/icons/icon-96.png">
<link rel="apple-touch-icon" href="/assets/icons/icon-192.png">

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
6) Register the Service Worker (COMPLETE)

Before </body>:

<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/service-worker.js").catch(console.error);
});
}
</script>
7) Install Button (NEVER DISAPPEARS)

Add a button anywhere (Navbar/Hero). Example:

<button type="button" id="installBtn" class="btn btn-primary">
<i class="fa-solid fa-download me-1"></i> Install App
</button>

Then add this script:

<script>
let deferredPrompt = null;
const installBtn = document.getElementById("installBtn");

// always show button
if (installBtn) installBtn.style.display = "inline-flex";

window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferredPrompt = e;
});

function installHelp(){
alert("Install haijaonekana bado. Chrome > Menu (⋮) > Add to Home screen ✅");
}

if (installBtn) {
installBtn.addEventListener("click", async () => {
if (!deferredPrompt) return installHelp();
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt = null;
});
}

window.addEventListener("appinstalled", () => {
// optional: hide button after installation
// installBtn.style.display = "none";
});
</script>
8) Testing Checklist (Important)

✅ Open your site in Chrome
✅ DevTools → Application tab

Manifest: should load correctly

Service Worker: active
✅ Turn off internet → refresh → should show cached index/offline page

If changes don’t apply:

Change cache version faulink-v1 → faulink-v2

DevTools → Application → Service Workers → Unregister → Reload

BONUS: Pro Tips

Cache only important pages (index, login pages, icons)

Don’t aggressively cache dynamic pages that require live DB

Keep “offline.html” lightweight and helpful

Use 192 + 512 icons always (mandatory for install on Android)

✅ Ready-to-Publish Conclusion (Premium)

With only three files (manifest.json, service-worker.js, and offline.html), you can turn your website into a professional installable app that works offline and feels like a native application.