Convert Any Website into an Installable App with PWA (Full Guide + Complete Working Files)
✅ 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.