Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    alinaqi

    pwa-development

    alinaqi/pwa-development
    Coding
    467
    2 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Progressive Web Apps - service workers, caching strategies, offline, Workbox

    SKILL.md

    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

    {
      "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)

    {
      "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

    // sw.js
    const CACHE_NAME = 'app-cache-v1';
    const STATIC_ASSETS = [
      '/',
      '/index.html',
      '/styles/main.css',
      '/scripts/app.js',
      '/offline.html'
    ];
    
    // Install: Cache static assets
    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then((cache) => cache.addAll(STATIC_ASSETS))
          .then(() => self.skipWaiting())
      );
    });
    
    // Activate: Clean old caches
    self.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 network
    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request)
          .then((cached) => cached || fetch(event.request))
          .catch(() => caches.match('/offline.html'))
      );
    });
    

    Registration

    // main.js
    if ('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)

    // Best for: Static assets that rarely change
    self.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)

    // Best for: API data, frequently updated content
    self.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

    // Best for: Content that's okay to be slightly outdated
    self.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

    npm install workbox-webpack-plugin  # Webpack
    npm install @vite-pwa/vite-plugin   # Vite
    

    Workbox with Vite

    // vite.config.js
    import { 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

    // sw.js
    import { 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 images
    registerRoute(
      ({ 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 responses
    registerRoute(
      ({ 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 navigations
    registerRoute(
      ({ request }) => request.mode === 'navigate',
      new NetworkFirst({
        cacheName: 'pages',
        plugins: [
          new CacheableResponsePlugin({ statuses: [0, 200] })
        ]
      })
    );
    

    Offline Experience

    Offline Page

    <!-- 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

    // Online/offline status handling
    function 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)

    // sw.js with Workbox
    import { 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'
    );
    
    // main.js - Queue form submission
    async 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

    let 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

    // Check if running as installed PWA
    function isInstalledPWA() {
      return window.matchMedia('(display-mode: standalone)').matches ||
             window.navigator.standalone === true; // iOS
    }
    
    // Listen for display mode changes
    window.matchMedia('(display-mode: standalone)')
      .addEventListener('change', (e) => {
        console.log('Display mode:', e.matches ? 'standalone' : 'browser');
      });
    

    Push Notifications

    // Request permission
    async function requestNotificationPermission() {
      const permission = await Notification.requestPermission();
      if (permission === 'granted') {
        await subscribeToPush();
      }
      return permission;
    }
    
    // Subscribe to push
    async 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 events
    self.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 click
    self.addEventListener('notificationclick', (event) => {
      event.notification.close();
      event.waitUntil(
        clients.openWindow(event.notification.data.url)
      );
    });
    

    Share Target

    // sw.js - Handle share target
    self.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

    <!-- 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

    <!-- 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

    // Dynamic imports for route-based splitting
    const 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

    # Run Lighthouse from CLI
    npx 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

    // Force update check
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.ready.then((registration) => {
        registration.update();
      });
    }
    
    // Listen for updates
    navigator.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

    npm install next-pwa
    
    // next.config.js
    const withPWA = require('next-pwa')({
      dest: 'public',
      disable: process.env.NODE_ENV === 'development'
    });
    
    module.exports = withPWA({
      // Your Next.js config
    });
    

    Create React App

    # CRA 4+ has PWA support built-in
    npx create-react-app my-pwa --template cra-template-pwa
    

    Vite (Any Framework)

    npm 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 First
    API Responses                        → Network First
    User-generated content              → Stale While Revalidate
    Analytics, non-cacheable            → Network Only
    Offline-only assets                 → Cache Only
    

    Manifest Minimum Requirements

    {
      "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
    
    Recommended Servers
    Cloudflare
    Cloudflare
    InstantDB
    InstantDB
    Svelte
    Svelte
    Repository
    alinaqi/claude-bootstrap
    Files