# Pwa Patterns

> When to activate: PWA, service worker, offline, cache strategy, Web App Manifest, push notifications, Workbox, install prompt

- Skill: `mattakushi432/pwa-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/pwa-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/pwa-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/pwa-patterns

---

# PWA Patterns

## Web App Manifest

```json
{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "screenshots": [
    { "src": "/screenshot-wide.png", "sizes": "1280x720", "form_factor": "wide" }
  ]
}
```

## Service Worker Registration

```js
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const reg = await navigator.serviceWorker.register('/sw.js');
      reg.addEventListener('updatefound', () => {
        const newSW = reg.installing;
        newSW.addEventListener('statechange', () => {
          if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
            showUpdateBanner(); // tell user to refresh
          }
        });
      });
    } catch (err) {
      console.error('SW registration failed:', err);
    }
  });
}
```

## Cache Strategies (Workbox)

```js
// sw.js
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';

// Precache built assets (injected by bundler plugin)
precacheAndRoute(self.__WB_MANIFEST);

// Cache-first: static assets (images, fonts)
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 })]
  })
);

// Network-first: API calls
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api', networkTimeoutSeconds: 3 })
);

// Stale-while-revalidate: HTML pages
registerRoute(
  new NavigationRoute(new StaleWhileRevalidate({ cacheName: 'pages' }))
);
```

## Offline Fallback

```js
// sw.js
const OFFLINE_PAGE = '/offline.html';

self.addEventListener('install', e => {
  e.waitUntil(caches.open('offline').then(c => c.add(OFFLINE_PAGE)));
});

self.addEventListener('fetch', e => {
  if (e.request.mode === 'navigate') {
    e.respondWith(
      fetch(e.request).catch(() => caches.match(OFFLINE_PAGE))
    );
  }
});
```

## Install Prompt (A2HS)

```js
let deferredPrompt;

window.addEventListener('beforeinstallprompt', e => {
  e.preventDefault();
  deferredPrompt = e;
  document.getElementById('install-btn').hidden = false;
});

document.getElementById('install-btn').addEventListener('click', async () => {
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  deferredPrompt = null;
  document.getElementById('install-btn').hidden = true;
});
```

## Push Notifications

```js
async function subscribePush(vapidPublicKey) {
  const reg = await navigator.serviceWorker.ready;
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(sub)
  });
}

// sw.js
self.addEventListener('push', e => {
  const data = e.data?.json() ?? {};
  e.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body, icon: '/icon-192.png', badge: '/badge.png'
    })
  );
});
```

## Vite PWA Plugin

```js
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';

export default {
  plugins: [VitePWA({
    registerType: 'autoUpdate',
    workbox: { globPatterns: ['**/*.{js,css,html,svg,png,woff2}'] },
    manifest: { name: 'My App', short_name: 'App', theme_color: '#2563eb' }
  })]
};
```

