PWA Setup
Gentelella v4 ships as a Progressive Web App by default — installable on macOS, Windows, iOS, and Android. Hand-written service worker, manifest, and offline fallback page. Cache strategy is network-first for HTML, cache-first for hashed assets.
Last updated May 22, 2026
Every Gentelella page is PWA-installable out of the box. There’s no vite-plugin-pwa, no Workbox config — the service worker and manifest are hand-written, deliberately small, and tunable from a couple of files.
What you get
- Install prompt on Chrome, Edge, Safari (16.4+), and mobile browsers
- Standalone window when installed — no browser chrome
- Offline fallback —
production/offline.htmldisplays when the network is unavailable - Smart caching — HTML is network-first (always fresh), hashed assets are cache-first (instant)
- App icons in the launcher / dock / home screen
- Shortcuts to Dashboard, Inbox, Calendar, Settings (long-press the icon on supported platforms)
- Cross-platform theme color — light + dark
<meta name="theme-color">for Chrome’s address bar
The three files
1. Manifest — public/site.webmanifest
{
"name": "Gentelella v4 — Admin Dashboard",
"short_name": "Gentelella",
"description": "Modern admin dashboard template — vanilla JS, real ECharts and DataTables, no Bootstrap, no jQuery.",
"icons": [
{ "src": "images/android-chrome-192x192.svg", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any" },
{ "src": "images/android-chrome-512x512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any" },
{ "src": "images/android-chrome-512x512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "maskable" }
],
"theme_color": "#1ABB9C",
"background_color": "#f5f7fb",
"display": "standalone",
"orientation": "any",
"scope": "./",
"start_url": "production/index.html",
"id": "gentelella-v4",
"lang": "en",
"categories": ["productivity", "business", "developer"],
"shortcuts": [
{ "name": "Dashboard", "url": "production/index.html" },
{ "name": "Inbox", "url": "production/inbox.html" },
{ "name": "Calendar", "url": "production/calendar.html" },
{ "name": "Settings", "url": "production/settings.html" }
]
}
Things worth noting:
- SVG icons instead of PNG. Smaller, sharper, theme-aware. Browsers handle them fine for both Android and iOS now.
- Relative
scope: "./"— the manifest works under any subpath without modification. shortcuts— the long-press app icon on Android/Windows shows these as quick actions. To add yours, append to the array.categories— affects discoverability in app stores that scrape manifests (Chrome’s PWA install panel, the Microsoft Store’s “Apps” tab).
To rebrand: rename, update icons, change theme_color and background_color to match your --primary and --body-bg tokens.
2. Service worker — public/sw.js
The worker is registered from main-v4.js only in production:
if ('serviceWorker' in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => {
const swPath = `${import.meta.env.BASE_URL}sw.js`;
navigator.serviceWorker.register(swPath).catch(() => { /* ignore */ });
});
}
import.meta.env.PROD guards against the dev server — you don’t want the SW caching HMR-served files in dev. BASE_URL resolves to whatever base Vite was configured with, so subpath deploys register the SW at the correct scope.
The worker itself is ~70 lines:
const CACHE = 'gentelella-v4-r2';
const SCOPE = self.registration?.scope || self.location.origin + '/';
const OFFLINE_URL = new URL('production/offline.html', SCOPE).href;
const PRECACHE = [OFFLINE_URL];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE)
.then((c) => c.addAll(PRECACHE).catch(() => {}))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return; // skip cross-origin
// HTML — network first, fallback to cache, then offline page
if (req.mode === 'navigate' || req.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(req)
.then((res) => {
caches.open(CACHE).then((c) => c.put(req, res.clone())).catch(() => {});
return res;
})
.catch(async () =>
(await caches.match(req)) || (await caches.match(OFFLINE_URL)) || Response.error()
)
);
return;
}
// Hashed assets — cache first
if (/\/(js|assets|images|fonts)\//.test(url.pathname)) {
event.respondWith(
caches.match(req).then((hit) => hit || fetch(req).then((res) => {
caches.open(CACHE).then((c) => c.put(req, res.clone())).catch(() => {});
return res;
}))
);
}
});
The strategy:
- HTML — network first. Always pull the freshest shell on every navigation. Falls back to cached HTML when offline, falls back to the offline page if not in cache. This is important: it means you never serve a stale HTML pointing at JS chunks that have since been hashed away.
- Hashed assets — cache first.
/js/,/assets/,/images/,/fonts/are content-hashed (Vite’s filename convention), so once an asset is cached it’s safe to serve forever. The Vite-emitted filename changes on every rebuild, so users naturally fetch new versions when they navigate to fresh HTML. - Cross-origin requests pass through. Google Fonts and any CDN you reference don’t go through the worker — that’s a deliberate scope choice to keep the SW small.
3. Offline page — production/offline.html
A simple branded fallback page served when the network is down and the requested page isn’t in the cache. The default looks like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Offline | Gentelella</title>
<!-- inline styles, no external requests -->
</head>
<body>
<div class="offline-shell">
<h1>You're offline</h1>
<p>Reconnect to load the dashboard.</p>
<button onclick="location.reload()">Try again</button>
</div>
</body>
</html>
The page is precached during the SW’s install event so it’s always available even on the first offline visit. If you want a branded offline experience, edit production/offline.html — anything you reference (CSS, images) needs to be inline or also precached.
Cache busting on release
Bump the CACHE constant in sw.js on every release:
const CACHE = 'gentelella-v4-r3'; // was 'gentelella-v4-r2'
The activate handler deletes any cache that doesn’t match the current name, so users transition cleanly to the new version on their next visit (no stuck-on-old-bundle issues).
Vite emits content-hashed filenames so the cache-first strategy never actually serves the wrong asset under a given URL — but bumping CACHE is the explicit “I want to flush everyone’s cache” lever for the rare time it’s needed.
Lighthouse / install criteria
To be installable, a PWA needs:
- ✅ HTTPS (or localhost)
- ✅ Service worker that controls the page
- ✅ Web app manifest linked from HTML with
name,short_name,start_url,display: standalone,icons(192px + 512px) - ✅ Maskable icon (Android adaptive)
- ✅
theme_color
Gentelella checks all five out of the box. Lighthouse PWA score is typically in the 90s on every page — the few points off are usually network-specific (no certificate, slow first paint over throttled 3G).
To verify locally:
npm run build
npm run preview
# In Chrome devtools → Application → Manifest, check for warnings
# Application → Service Workers, verify "activated and is running"
# Lighthouse tab → Run audit for PWA category
Note that the service worker only registers in production builds, so you have to run preview (not dev) to test SW behavior.
Disabling the PWA
Two ways:
1. Skip the SW registration. Edit main-v4.js, remove the if ('serviceWorker' in navigator…) block. The manifest still ships and pages remain installable as basic PWAs, but no offline support.
2. Remove everything. Delete public/sw.js, public/site.webmanifest, production/offline.html, the SW-registration block in main-v4.js, and the PWA meta-injection in the Vite plugin. The template still works fine — Vite just emits a plain dashboard.
Cross-platform notes
| Platform | PWA support |
|---|---|
| Chrome / Edge / Opera (desktop + Android) | Full — install prompt, shortcuts, push notifications, badging |
| Safari (macOS 16.4+, iOS 16.4+) | Install to dock/home screen, standalone window, manifest icons. No push, no shortcuts. |
| Firefox (desktop) | Limited — manifest read but no install prompt; SW works |
| Firefox (Android) | Adds to home screen via menu |
The template is conservative — it doesn’t use push notifications, periodic sync, or other Chrome-specific APIs. That keeps it portable. If you need those features, layer them in via notifications.html (already has a UI mockup) plus the Push API in sw.js.
See also
- Vite build — the build-time meta-tag injection that wires the manifest into every page
- Deployment — HTTPS hosting (required for PWAs) on Cloudflare Pages, Netlify, etc.