Claude Agent Skill · by Alinaqi

Pwa Development

Install Pwa Development skill for Claude Code from alinaqi/claude-bootstrap.

Install
Terminal · npx
$npx skills add https://github.com/alinaqi/claude-bootstrap --skill pwa-development
Works with Paperclip

How Pwa Development fits into a Paperclip company.

Pwa Development drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md960 lines
Expand
---name: pwa-developmentdescription: Progressive Web Apps - service workers, caching strategies, offline, Workboxwhen-to-use: When building PWA features - service workers, caching, offline supportuser-invocable: falsepaths: ["**/sw.*", "**/service-worker.*", "**/workbox-config.*", "**/manifest.json"]effort: medium--- # PWA Development Skill  **Purpose:** Build Progressive Web Apps that work offline, install like native apps, and deliver fast, reliable experiences across all devices. --- ## Core PWA Requirements ```┌─────────────────────────────────────────────────────────────────┐│  THE THREE PILLARS OF PWA                                       ││  ─────────────────────────────────────────────────────────────  ││                                                                 ││  1. HTTPS                                                       ││     Required for service workers and security.                  ││     localhost allowed for development.                          ││                                                                 ││  2. SERVICE WORKER                                              ││     JavaScript that runs in background.                         ││     Enables offline, caching, push notifications.               ││                                                                 ││  3. WEB APP MANIFEST                                            ││     JSON file describing app metadata.                          ││     Enables installation and app-like experience.               │├─────────────────────────────────────────────────────────────────┤│  INSTALLABILITY CRITERIA (Chrome)                               ││  ─────────────────────────────────────────────────────────────  ││  • HTTPS (or localhost)                                         ││  • Service worker with fetch handler                            ││  • Web app manifest with: name, icons (192px + 512px),          ││    start_url, display: standalone/fullscreen/minimal-ui         │└─────────────────────────────────────────────────────────────────┘``` --- ## Web App Manifest ### Required Fields ```json{  "name": "My Progressive Web App",  "short_name": "MyPWA",  "description": "A description of what the app does",  "start_url": "/",  "display": "standalone",  "background_color": "#ffffff",  "theme_color": "#000000",  "icons": [    {      "src": "/icons/icon-192.png",      "sizes": "192x192",      "type": "image/png"    },    {      "src": "/icons/icon-512.png",      "sizes": "512x512",      "type": "image/png"    },    {      "src": "/icons/icon-512-maskable.png",      "sizes": "512x512",      "type": "image/png",      "purpose": "maskable"    }  ]}``` ### Enhanced Manifest (Full Features) ```json{  "name": "My Progressive Web App",  "short_name": "MyPWA",  "description": "A full-featured PWA",  "start_url": "/?source=pwa",  "scope": "/",  "display": "standalone",  "orientation": "portrait-primary",  "background_color": "#ffffff",  "theme_color": "#3367D6",  "dir": "ltr",  "lang": "en",  "categories": ["productivity", "utilities"],   "icons": [    { "src": "/icons/icon-72.png", "sizes": "72x72", "type": "image/png" },    { "src": "/icons/icon-96.png", "sizes": "96x96", "type": "image/png" },    { "src": "/icons/icon-128.png", "sizes": "128x128", "type": "image/png" },    { "src": "/icons/icon-144.png", "sizes": "144x144", "type": "image/png" },    { "src": "/icons/icon-152.png", "sizes": "152x152", "type": "image/png" },    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },    { "src": "/icons/icon-384.png", "sizes": "384x384", "type": "image/png" },    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },    { "src": "/icons/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }  ],   "screenshots": [    {      "src": "/screenshots/desktop.png",      "sizes": "1280x720",      "type": "image/png",      "form_factor": "wide"    },    {      "src": "/screenshots/mobile.png",      "sizes": "750x1334",      "type": "image/png",      "form_factor": "narrow"    }  ],   "shortcuts": [    {      "name": "New Item",      "short_name": "New",      "description": "Create a new item",      "url": "/new?source=shortcut",      "icons": [{ "src": "/icons/shortcut-new.png", "sizes": "192x192" }]    }  ],   "share_target": {    "action": "/share",    "method": "POST",    "enctype": "multipart/form-data",    "params": {      "title": "title",      "text": "text",      "url": "url",      "files": [{ "name": "files", "accept": ["image/*"] }]    }  },   "protocol_handlers": [    {      "protocol": "web+myapp",      "url": "/handle?url=%s"    }  ],   "file_handlers": [    {      "action": "/open-file",      "accept": {        "text/plain": [".txt"]      }    }  ]}``` ### Manifest Checklist - [ ] `name` and `short_name` defined- [ ] `start_url` set (use query param for analytics)- [ ] `display` set to `standalone` or `fullscreen`- [ ] Icons: 192x192 and 512x512 minimum- [ ] Maskable icon included for Android adaptive icons- [ ] `theme_color` matches app design- [ ] `background_color` for splash screen- [ ] Screenshots for richer install UI (optional)- [ ] Shortcuts for quick actions (optional) --- ## Service Worker Patterns ### Basic Service Worker ```javascript// sw.jsconst CACHE_NAME = 'app-cache-v1';const STATIC_ASSETS = [  '/',  '/index.html',  '/styles/main.css',  '/scripts/app.js',  '/offline.html']; // Install: Cache static assetsself.addEventListener('install', (event) => {  event.waitUntil(    caches.open(CACHE_NAME)      .then((cache) => cache.addAll(STATIC_ASSETS))      .then(() => self.skipWaiting())  );}); // Activate: Clean old cachesself.addEventListener('activate', (event) => {  event.waitUntil(    caches.keys()      .then((keys) => Promise.all(        keys          .filter((key) => key !== CACHE_NAME)          .map((key) => caches.delete(key))      ))      .then(() => self.clients.claim())  );}); // Fetch: Serve from cache, fall back to networkself.addEventListener('fetch', (event) => {  event.respondWith(    caches.match(event.request)      .then((cached) => cached || fetch(event.request))      .catch(() => caches.match('/offline.html'))  );});``` ### Registration ```javascript// main.jsif ('serviceWorker' in navigator) {  window.addEventListener('load', async () => {    try {      const registration = await navigator.serviceWorker.register('/sw.js', {        scope: '/'      });      console.log('SW registered:', registration.scope);    } catch (error) {      console.error('SW registration failed:', error);    }  });}``` --- ## Caching Strategies ### Strategy Selection Guide | Strategy | Use Case | Description ||----------|----------|-------------|| **Cache First** | Static assets (CSS, JS, images) | Check cache, fall back to network || **Network First** | API responses, dynamic content | Try network, fall back to cache || **Stale While Revalidate** | Semi-static content (avatars, articles) | Serve cache immediately, update in background || **Network Only** | Non-cacheable requests (analytics) | Always use network || **Cache Only** | Offline-only assets | Only serve from cache | ### Cache First (Offline First) ```javascript// Best for: Static assets that rarely changeself.addEventListener('fetch', (event) => {  if (event.request.destination === 'image' ||      event.request.destination === 'style' ||      event.request.destination === 'script') {    event.respondWith(      caches.match(event.request)        .then((cached) => {          if (cached) return cached;          return fetch(event.request).then((response) => {            const clone = response.clone();            caches.open(CACHE_NAME).then((cache) => {              cache.put(event.request, clone);            });            return response;          });        })    );  }});``` ### Network First (Fresh First) ```javascript// Best for: API data, frequently updated contentself.addEventListener('fetch', (event) => {  if (event.request.url.includes('/api/')) {    event.respondWith(      fetch(event.request)        .then((response) => {          const clone = response.clone();          caches.open(CACHE_NAME).then((cache) => {            cache.put(event.request, clone);          });          return response;        })        .catch(() => caches.match(event.request))    );  }});``` ### Stale While Revalidate ```javascript// Best for: Content that's okay to be slightly outdatedself.addEventListener('fetch', (event) => {  if (event.request.url.includes('/articles/')) {    event.respondWith(      caches.open(CACHE_NAME).then((cache) => {        return cache.match(event.request).then((cached) => {          const fetchPromise = fetch(event.request).then((response) => {            cache.put(event.request, response.clone());            return response;          });          return cached || fetchPromise;        });      })    );  }});``` --- ## Workbox (Recommended) ### Why Workbox? - Battle-tested caching strategies- Precaching with revision management- Background sync for offline forms- Automatic cache cleanup- TypeScript support ### Installation ```bashnpm install workbox-webpack-plugin  # Webpacknpm install @vite-pwa/vite-plugin   # Vite``` ### Workbox with Vite ```javascript// vite.config.jsimport { VitePWA } from 'vite-plugin-pwa'; export default {  plugins: [    VitePWA({      registerType: 'autoUpdate',      includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],      manifest: {        name: 'My App',        short_name: 'App',        theme_color: '#ffffff',        icons: [          { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },          { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }        ]      },      workbox: {        globPatterns: ['**/*.{js,css,html,ico,png,svg}'],        runtimeCaching: [          {            urlPattern: /^https:\/\/api\.example\.com\/.*/i,            handler: 'NetworkFirst',            options: {              cacheName: 'api-cache',              expiration: {                maxEntries: 100,                maxAgeSeconds: 60 * 60 * 24 // 24 hours              }            }          },          {            urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,            handler: 'CacheFirst',            options: {              cacheName: 'image-cache',              expiration: {                maxEntries: 50,                maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days              }            }          }        ]      }    })  ]};``` ### Workbox Manual Service Worker ```javascript// sw.jsimport { precacheAndRoute } from 'workbox-precaching';import { registerRoute } from 'workbox-routing';import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';import { ExpirationPlugin } from 'workbox-expiration';import { CacheableResponsePlugin } from 'workbox-cacheable-response'; // Precache static assets (generated by build tool)precacheAndRoute(self.__WB_MANIFEST); // Cache imagesregisterRoute(  ({ request }) => request.destination === 'image',  new CacheFirst({    cacheName: 'images',    plugins: [      new CacheableResponsePlugin({ statuses: [0, 200] }),      new ExpirationPlugin({        maxEntries: 60,        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days      })    ]  })); // Cache API responsesregisterRoute(  ({ url }) => url.pathname.startsWith('/api/'),  new NetworkFirst({    cacheName: 'api-responses',    plugins: [      new CacheableResponsePlugin({ statuses: [0, 200] }),      new ExpirationPlugin({        maxEntries: 100,        maxAgeSeconds: 24 * 60 * 60 // 24 hours      })    ]  })); // Cache page navigationsregisterRoute(  ({ request }) => request.mode === 'navigate',  new NetworkFirst({    cacheName: 'pages',    plugins: [      new CacheableResponsePlugin({ statuses: [0, 200] })    ]  }));``` --- ## Offline Experience ### Offline Page ```html<!-- offline.html --><!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Offline - App Name</title>  <style>    body {      font-family: system-ui, sans-serif;      display: flex;      align-items: center;      justify-content: center;      min-height: 100vh;      margin: 0;      background: #f5f5f5;    }    .offline-content {      text-align: center;      padding: 2rem;    }    .offline-icon { font-size: 4rem; }    h1 { color: #333; }    p { color: #666; }    button {      background: #3367D6;      color: white;      border: none;      padding: 0.75rem 1.5rem;      border-radius: 4px;      cursor: pointer;      font-size: 1rem;    }  </style></head><body>  <div class="offline-content">    <div class="offline-icon">📡</div>    <h1>You're offline</h1>    <p>Check your connection and try again.</p>    <button onclick="location.reload()">Retry</button>  </div></body></html>``` ### Offline Detection ```javascript// Online/offline status handlingfunction updateOnlineStatus() {  const status = navigator.onLine ? 'online' : 'offline';  document.body.dataset.connectionStatus = status;   if (!navigator.onLine) {    showNotification('You are offline. Some features may be unavailable.');  }} window.addEventListener('online', updateOnlineStatus);window.addEventListener('offline', updateOnlineStatus);updateOnlineStatus();``` ### Background Sync (Queue Offline Actions) ```javascript// sw.js with Workboximport { BackgroundSyncPlugin } from 'workbox-background-sync';import { registerRoute } from 'workbox-routing';import { NetworkOnly } from 'workbox-strategies'; const bgSyncPlugin = new BackgroundSyncPlugin('formQueue', {  maxRetentionTime: 24 * 60 // Retry for 24 hours}); registerRoute(  ({ url }) => url.pathname === '/api/submit',  new NetworkOnly({    plugins: [bgSyncPlugin]  }),  'POST');``` ```javascript// main.js - Queue form submissionasync function submitForm(data) {  try {    const response = await fetch('/api/submit', {      method: 'POST',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify(data)    });    return response.json();  } catch (error) {    // Will be retried by background sync when online    showNotification('Saved offline. Will sync when connected.');  }}``` --- ## App-Like Features ### Install Prompt ```javascriptlet deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => {  e.preventDefault();  deferredPrompt = e;  showInstallButton();}); async function installApp() {  if (!deferredPrompt) return;   deferredPrompt.prompt();  const { outcome } = await deferredPrompt.userChoice;   console.log(`User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} install`);  deferredPrompt = null;  hideInstallButton();} window.addEventListener('appinstalled', () => {  console.log('App installed');  deferredPrompt = null;});``` ### Detecting Standalone Mode ```javascript// Check if running as installed PWAfunction isInstalledPWA() {  return window.matchMedia('(display-mode: standalone)').matches ||         window.navigator.standalone === true; // iOS} // Listen for display mode changeswindow.matchMedia('(display-mode: standalone)')  .addEventListener('change', (e) => {    console.log('Display mode:', e.matches ? 'standalone' : 'browser');  });``` ### Push Notifications ```javascript// Request permissionasync function requestNotificationPermission() {  const permission = await Notification.requestPermission();  if (permission === 'granted') {    await subscribeToPush();  }  return permission;} // Subscribe to pushasync function subscribeToPush() {  const registration = await navigator.serviceWorker.ready;  const subscription = await registration.pushManager.subscribe({    userVisibleOnly: true,    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)  });   // Send subscription to server  await fetch('/api/push/subscribe', {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify(subscription)  });} // sw.js - Handle push eventsself.addEventListener('push', (event) => {  const data = event.data.json();  event.waitUntil(    self.registration.showNotification(data.title, {      body: data.body,      icon: '/icons/icon-192.png',      badge: '/icons/badge-72.png',      data: { url: data.url }    })  );}); // Handle notification clickself.addEventListener('notificationclick', (event) => {  event.notification.close();  event.waitUntil(    clients.openWindow(event.notification.data.url)  );});``` ### Share Target ```javascript// sw.js - Handle share targetself.addEventListener('fetch', (event) => {  if (event.request.url.endsWith('/share') &&      event.request.method === 'POST') {    event.respondWith((async () => {      const formData = await event.request.formData();      const title = formData.get('title');      const text = formData.get('text');      const url = formData.get('url');       // Store or process shared content      // Redirect to app with shared data      return Response.redirect(`/?shared=true&title=${encodeURIComponent(title)}`);    })());  }});``` --- ## Performance Optimization ### Critical Rendering Path ```html<!-- Inline critical CSS --><style>  /* Critical above-the-fold styles */</style> <!-- Preload important resources --><link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="/scripts/app.js" as="script"> <!-- Defer non-critical CSS --><link rel="stylesheet" href="/styles/main.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="/styles/main.css"></noscript>``` ### Image Optimization ```html<!-- Responsive images --><img  src="/images/hero-800.webp"  srcset="    /images/hero-400.webp 400w,    /images/hero-800.webp 800w,    /images/hero-1200.webp 1200w  "  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"  alt="Hero image"  loading="lazy"  decoding="async"> <!-- Modern formats with fallback --><picture>  <source srcset="/images/hero.avif" type="image/avif">  <source srcset="/images/hero.webp" type="image/webp">  <img src="/images/hero.jpg" alt="Hero image" loading="lazy"></picture>``` ### Code Splitting ```javascript// Dynamic imports for route-based splittingconst routes = {  '/': () => import('./pages/Home.js'),  '/about': () => import('./pages/About.js'),  '/settings': () => import('./pages/Settings.js')}; async function loadPage(path) {  const loader = routes[path];  if (loader) {    const module = await loader();    return module.default;  }}``` --- ## Testing PWA ### Lighthouse Audit ```bash# Run Lighthouse from CLInpx lighthouse https://your-app.com --view # Key metrics to check:# - PWA badge (installable, offline-ready)# - Performance score# - Best practices# - Accessibility``` ### Manual Testing Checklist - [ ] **Installability**  - [ ] Install prompt appears on desktop Chrome  - [ ] Can be added to home screen on mobile  - [ ] App opens in standalone mode after install - [ ] **Offline Support**  - [ ] App loads when offline (airplane mode)  - [ ] Cached pages display correctly  - [ ] Offline fallback page shows for uncached routes  - [ ] Background sync works when coming back online - [ ] **Performance**  - [ ] First Contentful Paint < 1.8s  - [ ] Largest Contentful Paint < 2.5s  - [ ] Time to Interactive < 3.8s  - [ ] Cumulative Layout Shift < 0.1 - [ ] **Service Worker**  - [ ] SW registers successfully  - [ ] Static assets cached on install  - [ ] SW updates correctly (new version)  - [ ] No stale cache issues - [ ] **Manifest**  - [ ] All required fields present  - [ ] Icons display correctly  - [ ] Theme color applied  - [ ] Splash screen shows on launch ### Testing Service Worker Updates ```javascript// Force update checkif ('serviceWorker' in navigator) {  navigator.serviceWorker.ready.then((registration) => {    registration.update();  });} // Listen for updatesnavigator.serviceWorker.addEventListener('controllerchange', () => {  // New service worker activated  window.location.reload();});``` --- ## Project Structure ```project/├── public/│   ├── manifest.json           # Web app manifest│   ├── sw.js                   # Service worker (if not bundled)│   ├── offline.html            # Offline fallback page│   ├── robots.txt│   └── icons/│       ├── icon-72.png│       ├── icon-96.png│       ├── icon-128.png│       ├── icon-144.png│       ├── icon-152.png│       ├── icon-192.png│       ├── icon-384.png│       ├── icon-512.png│       ├── icon-maskable.png   # For adaptive icons│       ├── apple-touch-icon.png│       └── favicon.ico├── src/│   ├── sw.js                   # Service worker source (if bundled)│   ├── pwa/│   │   ├── install.js          # Install prompt handling│   │   ├── offline.js          # Offline detection│   │   └── push.js             # Push notification handling│   └── ...└── tests/    └── pwa/        ├── manifest.test.js        ├── sw.test.js        └── offline.test.js``` --- ## Common Mistakes | Mistake | Fix ||---------|-----|| Missing maskable icon | Add icon with `"purpose": "maskable"` || No offline fallback | Create `offline.html` and cache it || Cache never expires | Use `ExpirationPlugin` with Workbox || SW caches too aggressively | Use appropriate strategies per resource type || No update mechanism | Implement `skipWaiting()` + reload prompt || Broken install prompt | Ensure manifest meets all criteria || No HTTPS in production | Configure SSL certificate || Large cache size | Set `maxEntries` and `maxAgeSeconds` || Stale API responses | Use `NetworkFirst` for dynamic data || Missing start_url tracking | Add query param: `/?source=pwa` | --- ## PWA Development Checklist ### Before Launch - [ ] HTTPS configured (production)- [ ] Manifest complete with all required fields- [ ] Icons in all required sizes (192, 512, maskable)- [ ] Service worker registered and working- [ ] Offline page created and cached- [ ] Cache strategies defined for all resource types- [ ] Install prompt handling implemented- [ ] Lighthouse PWA audit passes ### After Launch - [ ] Monitor cache sizes- [ ] Test SW updates don't break app- [ ] Track PWA installs via analytics- [ ] Test on multiple devices/browsers- [ ] Monitor Core Web Vitals- [ ] Set up push notification flow (if needed) --- ## Framework-Specific Guides ### Next.js ```bashnpm install next-pwa``` ```javascript// next.config.jsconst withPWA = require('next-pwa')({  dest: 'public',  disable: process.env.NODE_ENV === 'development'}); module.exports = withPWA({  // Your Next.js config});``` ### Create React App ```bash# CRA 4+ has PWA support built-innpx create-react-app my-pwa --template cra-template-pwa``` ### Vite (Any Framework) ```bashnpm install vite-plugin-pwa -D``` See Workbox with Vite section above for configuration. --- ## Quick Reference ### Caching Strategy Cheat Sheet ```Static Assets (CSS, JS, images)     → Cache FirstAPI Responses                        → Network FirstUser-generated content              → Stale While RevalidateAnalytics, non-cacheable            → Network OnlyOffline-only assets                 → Cache Only``` ### Manifest Minimum Requirements ```json{  "name": "App Name",  "short_name": "App",  "start_url": "/",  "display": "standalone",  "icons": [    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }  ]}``` ### Service Worker Lifecycle ```1. Register → 2. Install → 3. Activate → 4. Fetch     ↓              ↓            ↓           ↓  Load app    Cache assets  Clean old   Serve requests                            caches      from cache/network```