PWA Installability
SkillDev toolsUse when the user asks to make a site installable, turn it into a PWA, add a service worker, add a web app manifest, enable "add to home screen", or support offline. Also use when adding favicon/app icons, iOS home-screen support (apple-mobile-web-app meta tags), or verifying PWA installability.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the PWA Installability skill
What this skill tells your AI
The instructions your AI receives, as published by spinspire/pocketbase-sveltekit-starter in .agents/skills/pwa/SKILL.md and read by ahel’s review.
Make a web app installable and iOS-friendly with a minimal service worker. Default posture is network-only (no caching) unless the app needs offline — pick a strategy deliberately, never by default.
When to Use
- "Make it installable / a PWA"
- "Add a service worker" / "add a web manifest" / "add to home screen"
- "Works offline?" or "works on iPhone?"
The Three Pillars
- HTTPS — required for service workers (localhost exempt in dev).
- Web app manifest — name, icons, start_url, display → makes it installable.
- Service worker — background script; active SW + valid manifest unlocks the install prompt.
Quick Reference
| Asset | Where it goes | Notes |
|---|---|---|
manifest.webmanifest | site root (static/ for SvelteKit) | name, short_name, start_url, scope, display standalone, theme/background color, icons 192+512 + maskable |
favicon-32/48/64.png | root | browser tab icons; also apple-touch-icon.png 180px for iOS |
icon-192.png, icon-512.png | root | required by Chrome for install |
icon-maskable-512.png | root | full-bleed background, content in center 80% safe zone |
sw.js | root (static/ for SvelteKit) | register from client code; served at site root for correct scope |
offline.html (optional) | root | fallback for failed navigations if offline support is wanted |
Service Worker
Default: network-only (no caching)
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});
This makes the app installable and always-fresh. Use for apps where data lives server-side / in a spreadsheet and serving stale data is wrong.
Offline fallback (add only if offline support is requested)
const CACHE = 'capstone-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then((c) => c.addAll(['/', '/offline.html', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png']))
);
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(() => clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (event.request.mode === 'navigate') {
event.respondWith(fetch(event.request).catch(() => caches.match('/offline.html')));
return;
}
if (url.origin === location.origin && ['css', 'js', 'svg', 'png', 'jpg', 'woff2'].some((ext) => url.pathname.endsWith('.' + ext))) {
event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request).then((res) => { const clone = res.clone(); caches.open(CACHE).then((c) => c.put(event.request, clone)); return res; })));
return;
}
event.respondWith(fetch(event.request));
});
Caching strategy decision table
| Strategy | Behavior | Use for |
|---|---|---|
| Network only | Always hit network, never cache | Auth, live/volatile data, spreadsheets, anything where stale is wrong. Default. |
| Cache first | Serve from cache, update in background | Static assets: css/js, fonts, images |
| Network first | Try network, fall back to cache | HTML pages, API reads where offline read > error |
| Stale while revalidate | Serve stale instantly, refresh cache | Semi-static content, lists |
When caching, version the cache name and delete old caches in activate. Never cache POST or auth'd API responses.
Registration (client side)
Register on mount, not at parse time, so it never delays first paint:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch((err) => console.warn('SW registration failed', err));
}
In SvelteKit, put this in +layout.svelte onMount. The SW must be at the site root (/sw.js) — static/sw.js ships there via adapter-static.
Manifest
{
"name": "Capstone — Construction business operations",
"short_name": "Capstone",
"description": "One-line description.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#faf9f6",
"theme_color": "#1f2933",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
Link it in the HTML head: <link rel="manifest" href="/manifest.webmanifest" />.
Icons from source images
- Source should be square (e.g. 768×768). Crop/resize with ImageMagick:
magick src.png -resize 192x192 icon-192.png magick src.png -resize 512x512 icon-512.png magick -size 512x512 xc:"#1f2933" \( src.png -resize 60% +repage \) -gravity center -composite icon-maskable-512.png - Maskable icons need a full-bleed background with the logo at ~60% inside the center safe zone.
iOS support (Safari / Add to Home Screen)
iOS ignores theme_color and needs legacy meta tags:
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="AppName" />
<meta name="theme-color" content="#1f2933" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
Caveats:
- iOS has no
beforeinstallprompt— users install via Share → Add to Home Screen. - iOS may evict service workers after ~7 days of inactivity (Intelligent Tracking Prevention) — expect re-registration on next visit.
Pre-ship audit
bun run check(or project typecheck) and build pass.- Start dev server, then verify with agent-browser:
- manifest + SW reachable:
curl -s -o /dev/null -w "%{http_code}" localhost:5173/manifest.webmanifest→ 200 - all icon assets return 200 (favicon-32/48/64, apple-touch-icon, icon-192/512, icon-maskable-512, sw.js)
- SW registered + active:
agent-browser eval "navigator.serviceWorker.getRegistration().then(r => ({ scope: r?.scope, active: r?.active?.state }))" - links present in head:
rel=icon,rel=apple-touch-icon,rel=manifest,meta[name=theme-color]
- manifest + SW reachable:
- Production requires HTTPS (Cloudflare Pages et al. provide it). Local dev on
http://localhostis exempt for SW registration. - Optional: run Lighthouse PWA audit in Chrome DevTools.
Common Mistakes
- Registering SW in
<head>or at module top-level — delays first paint; use onMount. - Caching auth'd or volatile API responses — serves stale or broken data. Keep those network-only.
- SW not at root —
/sw.jsonly controls the scope beneath it; a SW in/js/sw.jswon't control/. - Skipping maskable icon — Android adaptive icons will crop the logo without one.
- Cache name never versioned / old caches never deleted — disk fills and stale assets linger.
- Forgetting iOS meta tags — app opens in Safari tab instead of standalone, wrong status bar.
Signals
- GitHub stars
- 507
- Forks
- 76
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
pwa- Source
- github.com/spinspire/pocketbase-sveltekit-starter