Web App Manifest, Install & Offline UX
The manifest is the metadata the OS uses when installing your site. Combined with a Service Worker and HTTPS, it turns the page into a Progressive Web App.
manifest.webmanifest
{
"name": "DevRecall — Developer Knowledge",
"short_name": "DevRecall",
"description": "Organise your tech notes, bookmarks, and study material.",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0f0f12",
"theme_color": "#6a5acd",
"icons": [
{"src": "/icons/192.png", "sizes": "192x192", "type": "image/png"},
{"src": "/icons/512.png", "sizes": "512x512", "type": "image/png"},
{"src": "/icons/maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable"}
],
"shortcuts": [
{"name": "New page", "url": "/dashboard?new=page", "icons": [{"src": "/icons/new.png", "sizes": "96x96"}]}
]
}<!-- Link it from <head> on every page -->
<link rel="manifest" href="/manifest.webmanifest">
<!-- Browser UI theming -->
<meta name="theme-color" content="#6a5acd">
<!-- iOS-specific -->
<link rel="apple-touch-icon" href="/icons/192.png">
<meta name="apple-mobile-web-app-title" content="DevRecall">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">Installability Criteria
Served over HTTPS (or localhost for dev).
Has a manifest with name, short_name, start_url, display (standalone or fullscreen), and at least one 192px + 512px icon.
Registers a Service Worker with a fetch handler (Android Chrome). iOS does not require a SW for "Add to Home Screen".
User has interacted with the site (Chrome heuristic) before showing the prompt.
Custom Install Prompt
// Chrome fires `beforeinstallprompt` when criteria are met.
// Stash the event and trigger it from your UI when the user clicks Install.
let deferred: any = null
window.addEventListener('beforeinstallprompt', (e: Event) => {
e.preventDefault() // suppress the default mini-infobar
deferred = e
showInstallButton()
})
async function onInstallClick() {
if (!deferred) return
deferred.prompt()
const {outcome} = await deferred.userChoice // 'accepted' | 'dismissed'
console.log('install:', outcome)
deferred = null
hideInstallButton()
}
window.addEventListener('appinstalled', () => {
console.log('installed!')
hideInstallButton()
})
// iOS Safari has no programmatic install — show a one-time hint:
const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent)
const isStandalone = (window.navigator as any).standalone === true
if (isIOS && !isStandalone) showAddToHomeScreenHint()Offline UX
// Detect online status — kept in sync with the OS
window.addEventListener('online', () => updateBanner('back online'))
window.addEventListener('offline', () => updateBanner('offline mode'))
if (!navigator.onLine) updateBanner('offline mode')
// Three patterns worth knowing:
// 1) Offline fallback page (served from cache when navigation fails)
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then(c => c.add('/offline.html')))
})
// 2) Background Sync — replay failed POSTs once the user is back online
const reg = await navigator.serviceWorker.ready
await reg.sync.register('replay-queued-actions')
// In sw.js:
self.addEventListener('sync', (e) => {
if (e.tag === 'replay-queued-actions') {
e.waitUntil(replayFromIndexedDB())
}
})
// 3) IndexedDB for app state (small) — Cache API for static assets (large).
// Don't store gigabytes in Cache; browsers evict aggressively under quota.Push Notifications (Web Push)
// 1) Ask permission only after the user expresses intent (button click).
const perm = await Notification.requestPermission()
if (perm !== 'granted') return
// 2) Subscribe — needs VAPID keys generated server-side
const reg = await navigator.serviceWorker.ready
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true, // required
applicationServerKey: urlBase64ToUint8(VAPID_PUBLIC_KEY),
})
// 3) Ship `sub.toJSON()` to your backend; store per user.
await fetch('/api/push/subscribe', {method: 'POST', body: JSON.stringify(sub)})
// 4) Server signs and sends with web-push library; SW handles it:
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body, icon: '/icons/192.png', data: {url: data.url},
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(clients.openWindow(event.notification.data?.url ?? '/'))
})
// iOS supports Web Push only when installed as a PWA (Safari 16.4+).Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free