Progressive Web Applications (PWA)

Build modern, reliable, and engaging Progressive Web Applications with offline capabilities

Key Facts

Category
Web Technologies
Items
4
Format Families
json

Sample Overview

Build modern, reliable, and engaging Progressive Web Applications with offline capabilities This sample set belongs to Web Technologies and can be used to test related workflows inside Elysia Tools.

💻 PWA Manifest and Installation json

🟢 simple ⭐⭐

Create a PWA manifest and enable app installation

⏱️ 15 min 🏷️ pwa, manifest, installation, app shell
Prerequisites: HTML, Web development basics
{
  "manifest.json": {
    "name": "My Progressive Web App",
    "short_name": "MyPWA",
    "description": "A modern Progressive Web Application",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#3367D6",
    "orientation": "portrait-primary",
    "scope": "/",
    "lang": "en",
    "dir": "ltr",
    "categories": ["productivity", "utilities"],
    "screenshots": [
      {
        "src": "/screenshots/desktop-1.png",
        "sizes": "1280x720",
        "type": "image/png",
        "form_factor": "wide",
        "label": "Desktop view of the app"
      },
      {
        "src": "/screenshots/mobile-1.png",
        "sizes": "375x667",
        "type": "image/png",
        "form_factor": "narrow",
        "label": "Mobile view of the app"
      }
    ],
    "icons": [
      {
        "src": "/icons/icon-72x72.png",
        "sizes": "72x72",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-96x96.png",
        "sizes": "96x96",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-128x128.png",
        "sizes": "128x128",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-144x144.png",
        "sizes": "144x144",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-152x152.png",
        "sizes": "152x152",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-192x192.png",
        "sizes": "192x192",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-384x384.png",
        "sizes": "384x384",
        "type": "image/png",
        "purpose": "maskable any"
      },
      {
        "src": "/icons/icon-512x512.png",
        "sizes": "512x512",
        "type": "image/png",
        "purpose": "maskable any"
      }
    ],
    "shortcuts": [
      {
        "name": "New Task",
        "short_name": "New Task",
        "description": "Create a new task quickly",
        "url": "/new-task",
        "icons": [
          {
            "src": "/icons/shortcut-new.png",
            "sizes": "96x96"
          }
        ]
      },
      {
        "name": "Search",
        "short_name": "Search",
        "description": "Search your tasks",
        "url": "/search",
        "icons": [
          {
            "src": "/icons/shortcut-search.png",
            "sizes": "96x96"
          }
        ]
      }
    ],
    "related_applications": [
      {
        "platform": "play",
        "url": "https://play.google.com/store/apps/details?id=com.example.app",
        "id": "com.example.app"
      }
    ],
    "prefer_related_applications": false,
    "share_target": {
      "action": "/share-handler",
      "method": "POST",
      "enctype": "multipart/form-data",
      "params": {
        "title": "title",
        "text": "text",
        "url": "url",
        "files": [
          {
            "name": "images",
            "accept": ["image/*"]
          }
        ]
      }
    },
    "protocol_handlers": [
      {
        "protocol": "web+pwa",
        "url": "/handle-protocol?type=%s"
      }
    ]
  }
}

<!-- HTML head section -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Progressive Web App</title>

  <!-- Theme color -->
  <meta name="theme-color" content="#3367D6">

  <!-- Apple specific meta tags -->
  <meta name="apple-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="MyPWA">

  <!-- Apple icons -->
  <link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png">
  <link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png">
  <link rel="apple-touch-icon" sizes="128x128" href="/icons/icon-128x128.png">
  <link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png">
  <link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png">
  <link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png">
  <link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png">
  <link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png">

  <!-- PWA manifest -->
  <link rel="manifest" href="/manifest.json">

  <!-- Splash screen images -->
  <link rel="apple-touch-startup-image"
        media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)"
        href="/screenshots/apple-launch-828x1792.png">
  <link rel="apple-touch-startup-image"
        media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)"
        href="/screenshots/apple-launch-1125x2436.png">
  <!-- Add more device-specific splash screens -->

  <!-- Styles and scripts -->
  <link rel="stylesheet" href="/styles/main.css">
</head>
<body>
  <div id="app">
    <header>
      <h1>My Progressive Web App</h1>
      <div id="install-prompt" style="display: none;">
        <button id="install-button">Install App</button>
      </div>
    </header>
    <main>
      <!-- App content here -->
    </main>
  </div>

  <script src="/scripts/app.js"></script>
</body>
</html>

💻 PWA Install Prompt Management javascript

🟡 intermediate ⭐⭐⭐

Handle app installation prompts and user experience

⏱️ 20 min 🏷️ pwa, installation, user experience, ios
Prerequisites: JavaScript, PWA basics, Event handling
// PWA Install Prompt Management
class PWAInstallManager {
  constructor() {
    this.deferredPrompt = null;
    this.installButton = document.getElementById('install-button');
    this.installPrompt = document.getElementById('install-prompt');

    this.init();
  }

  init() {
    // Listen for beforeinstallprompt event
    window.addEventListener('beforeinstallprompt', (e) => {
      // Prevent the mini-infobar from appearing on mobile
      e.preventDefault();

      // Stash the event so it can be triggered later
      this.deferredPrompt = e;

      // Show install button
      this.showInstallPrompt();

      console.log('beforeinstallprompt event fired');
    });

    // Listen for app installed event
    window.addEventListener('appinstalled', () => {
      // Hide install button
      this.hideInstallPrompt();

      // Clear the deferred prompt
      this.deferredPrompt = null;

      // Show success message
      this.showInstallSuccess();

      console.log('PWA was installed');

      // Analytics: track installation
      this.trackInstallEvent();
    });

    // Check if app is already installed
    if (this.isAppInstalled()) {
      this.hideInstallPrompt();
      this.showAlreadyInstalled();
    }

    // Handle install button click
    if (this.installButton) {
      this.installButton.addEventListener('click', () => {
        this.installPWA();
      });
    }

    // Check for iOS devices (no beforeinstallprompt event)
    if (this.isIOS()) {
      this.showIOSInstallInstructions();
    }

    // Listen for standalone mode changes
    window.addEventListener('appmodechange', () => {
      this.handleAppModeChange();
    });

    // Periodically check installability
    this.checkInstallability();
  }

  showInstallPrompt() {
    if (this.installPrompt) {
      // Don't show if user has dismissed before
      const dismissed = localStorage.getItem('installPromptDismissed');
      if (!dismissed) {
        this.installPrompt.style.display = 'block';
      }
    }
  }

  hideInstallPrompt() {
    if (this.installPrompt) {
      this.installPrompt.style.display = 'none';
    }
  }

  async installPWA() {
    if (!this.deferredPrompt) {
      console.log('No deferred prompt available');
      return;
    }

    try {
      // Show the install prompt
      this.deferredPrompt.prompt();

      // Wait for the user to respond to the prompt
      const { outcome } = await this.deferredPrompt.userChoice;

      console.log('User response to the install prompt:', outcome);

      // We've used the prompt, and can't use it again, throw it away
      this.deferredPrompt = null;

      if (outcome === 'accepted') {
        console.log('User accepted the install prompt');
        // Analytics: track acceptance
        this.trackInstallEvent('accepted');
      } else {
        console.log('User dismissed the install prompt');
        // Analytics: track dismissal
        this.trackInstallEvent('dismissed');
        // Hide for a while
        this.dismissInstallPrompt();
      }
    } catch (error) {
      console.error('Error during PWA installation:', error);
    }
  }

  dismissInstallPrompt() {
    this.hideInstallPrompt();
    localStorage.setItem('installPromptDismissed', 'true');

    // Show again after 7 days
    setTimeout(() => {
      localStorage.removeItem('installPromptDismissed');
      if (this.deferredPrompt) {
        this.showInstallPrompt();
      }
    }, 7 * 24 * 60 * 60 * 1000);
  }

  showInstallSuccess() {
    this.showNotification('App installed successfully!', 'success');
  }

  showAlreadyInstalled() {
    console.log('App is already installed');
  }

  showIOSInstallInstructions() {
    // iOS doesn't support beforeinstallprompt
    // Show custom instructions for Safari
    const userAgent = navigator.userAgent.toLowerCase();
    if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
      const iosPrompt = document.createElement('div');
      iosPrompt.className = 'ios-install-prompt';
      iosPrompt.innerHTML = `
        <div class="ios-install-content">
          <h3>Install this App</h3>
          <p>To install this app on your iOS device:</p>
          <ol>
            <li>Tap the Share button <span class="share-icon">⎋</span> in Safari</li>
            <li>Scroll down and tap "Add to Home Screen"</li>
            <li>Tap "Add" to confirm</li>
          </ol>
          <button class="close-btn">Got it!</button>
        </div>
      `;

      document.body.appendChild(iosPrompt);

      // Show after a delay
      setTimeout(() => {
        iosPrompt.classList.add('show');
      }, 3000);

      // Handle close button
      iosPrompt.querySelector('.close-btn').addEventListener('click', () => {
        iosPrompt.classList.remove('show');
        setTimeout(() => {
          document.body.removeChild(iosPrompt);
        }, 300);
      });

      // Auto-hide after 10 seconds
      setTimeout(() => {
        if (iosPrompt.parentNode) {
          iosPrompt.classList.remove('show');
          setTimeout(() => {
            if (iosPrompt.parentNode) {
              document.body.removeChild(iosPrompt);
            }
          }, 300);
        }
      }, 10000);
    }
  }

  isAppInstalled() {
    // Check if running in standalone mode
    return window.matchMedia('(display-mode: standalone)').matches ||
           window.navigator.standalone === true ||
           document.referrer.includes('android-app://');
  }

  isIOS() {
    return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
  }

  handleAppModeChange() {
    if (this.isAppInstalled()) {
      // App is now running in standalone mode
      document.body.classList.add('standalone-mode');

      // Enable app-specific features
      this.enableStandaloneFeatures();
    } else {
      document.body.classList.remove('standalone-mode');
    }
  }

  enableStandaloneFeatures() {
    // Enable features only available in standalone mode
    console.log('Enabling standalone mode features');

    // Example: Show back button navigation
    this.addBackButton();

    // Example: Customize status bar
    this.customizeStatusBar();
  }

  addBackButton() {
    if (document.referrer && document.referrer !== window.location.href) {
      const backButton = document.createElement('button');
      backButton.className = 'back-button';
      backButton.innerHTML = '←';
      backButton.addEventListener('click', () => {
        window.history.back();
      });

      document.body.insertBefore(backButton, document.body.firstChild);
    }
  }

  customizeStatusBar() {
    // This works on iOS Safari
    const metaTheme = document.querySelector('meta[name="theme-color"]');
    if (metaTheme && this.isIOS()) {
      metaTheme.content = '#000000'; // Dark status bar for iOS
    }
  }

  async checkInstallability() {
    // Check if PWA meets installability criteria
    const isInstallable = await this.isPWAInstallable();

    if (!isInstallable) {
      console.warn('PWA may not be installable due to missing requirements');
      this.showInstallabilityIssues();
    }
  }

  async isPWAInstallable() {
    try {
      // Check if served over HTTPS
      if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
        return false;
      }

      // Check for service worker
      if (!('serviceWorker' in navigator)) {
        return false;
      }

      // Check for manifest
      const manifest = await fetch('/manifest.json').then(r => r.json()).catch(() => null);
      if (!manifest) {
        return false;
      }

      // Check for icons
      if (!manifest.icons || manifest.icons.length === 0) {
        return false;
      }

      // Check for suitable icon size
      const hasLargeIcon = manifest.icons.some(icon =>
        icon.sizes && icon.sizes.split('x').some(size => parseInt(size) >= 192)
      );
      if (!hasLargeIcon) {
        return false;
      }

      // Check if service worker is registered
      const registration = await navigator.serviceWorker.ready;
      if (!registration) {
        return false;
      }

      return true;
    } catch (error) {
      console.error('Error checking installability:', error);
      return false;
    }
  }

  showInstallabilityIssues() {
    const issues = [];

    if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
      issues.push('Serve over HTTPS');
    }

    if (!('serviceWorker' in navigator)) {
      issues.push('Service Worker support');
    }

    // Add more checks as needed

    if (issues.length > 0) {
      console.warn('PWA installability issues:', issues);
    }
  }

  showNotification(message, type = 'info') {
    const notification = document.createElement('div');
    notification.className = `pwa-notification pwa-notification--${type}`;
    notification.textContent = message;

    document.body.appendChild(notification);

    setTimeout(() => {
      notification.classList.add('show');
    }, 100);

    setTimeout(() => {
      notification.classList.remove('show');
      setTimeout(() => {
        if (notification.parentNode) {
          document.body.removeChild(notification);
        }
      }, 300);
    }, 3000);
  }

  trackInstallEvent(action = 'prompt') {
    // Analytics tracking for install events
    if (window.gtag) {
      gtag('event', 'pwa_install', {
        'event_category': 'PWA',
        'event_label': action,
        'value': 1
      });
    }
  }
}

// Usage
document.addEventListener('DOMContentLoaded', () => {
  new PWAInstallManager();
});

💻 Offline-First PWA Architecture javascript

🟡 intermediate ⭐⭐⭐⭐

Build a robust offline-first Progressive Web Application

⏱️ 35 min 🏷️ offline-first, pwa, indexeddb, sync, architecture
Prerequisites: JavaScript, IndexedDB, Service Workers, PWA concepts
// Offline-First PWA Architecture
class OfflineFirstPWA {
  constructor() {
    this.db = null;
    this.onlineStatus = navigator.onLine;
    this.pendingOperations = new Map();

    this.init();
  }

  async init() {
    // Initialize IndexedDB
    await this.initDatabase();

    // Setup network status listeners
    this.setupNetworkListeners();

    // Register service worker
    await this.registerServiceWorker();

    // Setup sync manager
    this.setupBackgroundSync();

    // Load cached data
    await this.loadOfflineData();

    console.log('Offline-First PWA initialized');
  }

  async initDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('offline-pwa-db', 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        // Store for cached API responses
        if (!db.objectStoreNames.contains('api-cache')) {
          db.createObjectStore('api-cache', { keyPath: 'url' });
        }

        // Store for user data
        if (!db.objectStoreNames.contains('user-data')) {
          db.createObjectStore('user-data', { keyPath: 'id', autoIncrement: true });
        }

        // Store for pending operations
        if (!db.objectStoreNames.contains('pending-operations')) {
          db.createObjectStore('pending-operations', { keyPath: 'id', autoIncrement: true });
        }

        // Store for offline files
        if (!db.objectStoreNames.contains('offline-files')) {
          db.createObjectStore('offline-files', { keyPath: 'url' });
        }
      };
    });
  }

  setupNetworkListeners() {
    window.addEventListener('online', () => {
      this.onlineStatus = true;
      this.handleOnlineStatus();
      console.log('App is online');
    });

    window.addEventListener('offline', () => {
      this.onlineStatus = false;
      this.handleOfflineStatus();
      console.log('App is offline');
    });

    // Update UI based on current status
    this.updateOnlineStatusUI();
  }

  handleOnlineStatus() {
    // Show online indicator
    this.showStatus('Connected', 'online');

    // Process pending operations
    this.processPendingOperations();

    // Sync data with server
    this.syncWithServer();

    // Refresh cached data
    this.refreshStaleData();
  }

  handleOfflineStatus() {
    // Show offline indicator
    this.showStatus('Offline - Limited functionality', 'offline');

    // Switch to offline mode
    this.enableOfflineMode();

    // Warn about limitations
    this.warnOfflineLimitations();
  }

  async registerServiceWorker() {
    if ('serviceWorker' in navigator) {
      try {
        const registration = await navigator.serviceWorker.register('/offline-pwa-sw.js');

        console.log('Service Worker registered:', registration);

        // Listen for messages from service worker
        navigator.serviceWorker.addEventListener('message', (event) => {
          this.handleServiceWorkerMessage(event);
        });

        return registration;
      } catch (error) {
        console.error('Service Worker registration failed:', error);
      }
    }
  }

  setupBackgroundSync() {
    if ('serviceWorker' in navigator &&
        typeof ServiceWorkerRegistration !== 'undefined' &&
        'sync' in ServiceWorkerRegistration.prototype) {
      navigator.serviceWorker.ready.then((registration) => {
        registration.sync.register('offline-data-sync');
      });
    }
  }

  async loadOfflineData() {
    try {
      // Load essential user data from IndexedDB
      const userData = await this.getAllUserData();
      this.renderUserData(userData);

      // Load cached API responses
      const cachedData = await this.getCachedData();
      this.renderCachedData(cachedData);

    } catch (error) {
      console.error('Error loading offline data:', error);
    }
  }

  // API methods with offline support
  async apiRequest(url, options = {}) {
    const cacheKey = url + JSON.stringify(options);

    if (!this.onlineStatus) {
      // Offline: return cached data or create pending operation
      const cachedResponse = await this.getCachedResponse(url);

      if (cachedResponse) {
        return cachedResponse;
      }

      if (options.method && options.method !== 'GET') {
        // Create pending operation for non-GET requests
        return this.createPendingOperation(url, options);
      }

      throw new Error('No cached data available');
    }

    try {
      // Online: make actual request
      const response = await fetch(url, options);

      if (response.ok) {
        const data = await response.json();

        // Cache the response
        await this.cacheResponse(url, data);

        return data;
      }

      throw new Error('API request failed');

    } catch (error) {
      console.error('API request failed:', error);

      // Fallback to cached data
      const cachedResponse = await this.getCachedResponse(url);

      if (cachedResponse) {
        this.showStatus('Using cached data', 'warning');
        return cachedResponse;
      }

      throw error;
    }
  }

  async cacheResponse(url, data) {
    const transaction = this.db.transaction(['api-cache'], 'readwrite');
    const store = transaction.objectStore('api-cache');

    const cacheEntry = {
      url: url,
      data: data,
      timestamp: Date.now(),
      ttl: Date.now() + (5 * 60 * 1000) // 5 minutes TTL
    };

    await store.put(cacheEntry);
  }

  async getCachedResponse(url) {
    const transaction = this.db.transaction(['api-cache'], 'readonly');
    const store = transaction.objectStore('api-cache');

    const cacheEntry = await store.get(url);

    if (cacheEntry && Date.now() < cacheEntry.ttl) {
      return cacheEntry.data;
    }

    return null;
  }

  async createPendingOperation(url, options) {
    const operation = {
      id: Date.now().toString(),
      url: url,
      options: options,
      timestamp: Date.now(),
      retries: 0
    };

    const transaction = this.db.transaction(['pending-operations'], 'readwrite');
    const store = transaction.objectStore('pending-operations');

    await store.add(operation);

    this.showStatus('Operation queued for sync', 'info');

    return { success: true, message: 'Operation queued for sync', operationId: operation.id };
  }

  async processPendingOperations() {
    const transaction = this.db.transaction(['pending-operations'], 'readonly');
    const store = transaction.objectStore('pending-operations');

    const operations = await store.getAll();

    for (const operation of operations) {
      try {
        await this.retryOperation(operation);
        await this.removePendingOperation(operation.id);
      } catch (error) {
        console.error('Failed to process operation:', operation.id, error);
      }
    }
  }

  async retryOperation(operation) {
    const maxRetries = 3;

    if (operation.retries >= maxRetries) {
      throw new Error('Max retries exceeded');
    }

    try {
      const response = await fetch(operation.url, operation.options);

      if (!response.ok) {
        throw new Error('Request failed');
      }

      // Operation successful
      console.log('Operation synced successfully:', operation.id);

    } catch (error) {
      // Increment retry count
      operation.retries++;

      // Update operation in database
      const transaction = this.db.transaction(['pending-operations'], 'readwrite');
      const store = transaction.objectStore('pending-operations');
      await store.put(operation);

      throw error;
    }
  }

  async removePendingOperation(id) {
    const transaction = this.db.transaction(['pending-operations'], 'readwrite');
    const store = transaction.objectStore('pending-operations');
    await store.delete(id);
  }

  // File handling for offline support
  async cacheFile(url, file) {
    const fileData = {
      url: url,
      file: file,
      timestamp: Date.now()
    };

    const transaction = this.db.transaction(['offline-files'], 'readwrite');
    const store = transaction.objectStore('offline-files');

    await store.put(fileData);
  }

  async getCachedFile(url) {
    const transaction = this.db.transaction(['offline-files'], 'readonly');
    const store = transaction.objectStore('pending-operations');

    const fileData = await store.get(url);

    return fileData ? fileData.file : null;
  }

  // UI Methods
  updateOnlineStatusUI() {
    const statusElement = document.getElementById('online-status');
    if (statusElement) {
      statusElement.className = this.onlineStatus ? 'online' : 'offline';
      statusElement.textContent = this.onlineStatus ? 'Online' : 'Offline';
    }
  }

  showStatus(message, type = 'info') {
    const statusElement = document.getElementById('status-message');
    if (statusElement) {
      statusElement.textContent = message;
      statusElement.className = `status status--${type}`;

      setTimeout(() => {
        statusElement.textContent = '';
        statusElement.className = 'status';
      }, 3000);
    }
  }

  showNotification(title, options = {}) {
    if ('Notification' in window && Notification.permission === 'granted') {
      new Notification(title, {
        body: options.body || '',
        icon: '/icons/icon-192x192.png',
        badge: '/icons/badge-72x72.png',
        tag: options.tag || 'default',
        requireInteraction: options.requireInteraction || false,
        actions: options.actions || []
      });
    }
  }

  enableOfflineMode() {
    // Enable offline-specific features
    document.body.classList.add('offline-mode');

    // Disable online-only features
    const onlineOnlyElements = document.querySelectorAll('[data-online-only]');
    onlineOnlyElements.forEach(element => {
      element.style.display = 'none';
    });

    // Show offline indicators
    const offlineIndicators = document.querySelectorAll('[data-offline-indicator]');
    offlineIndicators.forEach(element => {
      element.style.display = 'block';
    });
  }

  warnOfflineLimitations() {
    this.showNotification('You are currently offline', {
      body: 'Some features may be unavailable until you reconnect.',
      requireInteraction: false,
      tag: 'offline-warning'
    });
  }

  // Data synchronization
  async syncWithServer() {
    try {
      // Get local changes
      const localChanges = await this.getLocalChanges();

      // Sync with server
      const syncResult = await this.sendChangesToServer(localChanges);

      if (syncResult.success) {
        // Clear local changes after successful sync
        await this.clearLocalChanges();
        this.showStatus('Data synchronized successfully', 'success');
      }

    } catch (error) {
      console.error('Sync failed:', error);
      this.showStatus('Sync failed, will retry later', 'warning');
    }
  }

  async refreshStaleData() {
    // Refresh data that might be stale
    const staleData = await this.getStaleCachedData();

    for (const item of staleData) {
      try {
        await this.apiRequest(item.url, { method: 'GET' });
      } catch (error) {
        console.error('Failed to refresh stale data:', item.url);
      }
    }
  }

  handleServiceWorkerMessage(event) {
    const { type, data } = event.data;

    switch (type) {
      case 'cache-updated':
        this.handleCacheUpdated(data);
        break;
      case 'sync-completed':
        this.handleSyncCompleted(data);
        break;
      case 'background-sync-failed':
        this.handleBackgroundSyncFailed(data);
        break;
      default:
        console.log('Unknown message from service worker:', type, data);
    }
  }

  handleCacheUpdated(data) {
    console.log('Cache updated:', data);
    this.showStatus('Content updated', 'success');
  }

  handleSyncCompleted(data) {
    console.log('Background sync completed:', data);
    this.showNotification('Sync completed', {
      body: 'Your data has been synchronized with the server.'
    });
  }

  handleBackgroundSyncFailed(data) {
    console.error('Background sync failed:', data);
    this.showStatus('Background sync failed', 'error');
  }
}

// Initialize the PWA
document.addEventListener('DOMContentLoaded', () => {
  window.pwaApp = new OfflineFirstPWA();
});

// Service Worker file (offline-pwa-sw.js)
const OFFLINE_CACHE = 'offline-cache-v1';
const STATIC_CACHE = 'static-cache-v1';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(OFFLINE_CACHE)
      .then((cache) => cache.addAll([
        '/',
        '/offline.html',
        '/manifest.json',
        '/styles/main.css',
        '/scripts/app.js'
      ]))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        return response || fetch(event.request);
      })
      .catch(() => {
        // Return offline page for navigation requests
        if (event.request.mode === 'navigate') {
          return caches.match('/offline.html');
        }
      })
  );
});

self.addEventListener('sync', (event) => {
  if (event.tag === 'offline-data-sync') {
    event.waitUntil(
      // Process offline data sync
      processOfflineDataSync()
    );
  }
});

async function processOfflineDataSync() {
  // Implementation for background sync
  console.log('Processing offline data sync');
}

💻 PWA App Shell Architecture javascript

🔴 complex ⭐⭐⭐⭐⭐

Implement the app shell architecture for instant loading and reliable navigation

⏱️ 40 min 🏷️ app shell, pwa, architecture, routing, performance
Prerequisites: JavaScript, PWA concepts, Web Components, Service Workers
// PWA App Shell Architecture
class PWAShell {
  constructor() {
    this.currentRoute = null;
    this.shellElement = null;
    this.contentElement = null;
    this.routes = new Map();
    this.loadingIndicator = null;

    this.init();
  }

  init() {
    // Cache DOM elements
    this.shellElement = document.getElementById('app-shell');
    this.contentElement = document.getElementById('app-content');
    this.loadingIndicator = document.getElementById('loading-indicator');

    // Setup routing
    this.setupRouting();

    // Setup navigation
    this.setupNavigation();

    // Setup service worker communication
    this.setupServiceWorkerComms();

    // Handle initial load
    this.handleInitialLoad();

    // Setup prefetching
    this.setupPrefetching();

    // Setup back/forward navigation
    this.setupHistoryNavigation();

    console.log('PWA App Shell initialized');
  }

  setupRouting() {
    // Define app routes
    this.routes.set('/', {
      component: 'HomePage',
      prefetch: true,
      cache: true,
      preload: ['/api/user-data', '/api/recent-items']
    });

    this.routes.set('/dashboard', {
      component: 'DashboardPage',
      prefetch: true,
      cache: true,
      preload: ['/api/dashboard-stats', '/api/notifications']
    });

    this.routes.set('/profile', {
      component: 'ProfilePage',
      prefetch: false,
      cache: true,
      preload: ['/api/user-profile']
    });

    this.routes.set('/settings', {
      component: 'SettingsPage',
      prefetch: false,
      cache: true,
      preload: ['/api/settings']
    });

    this.routes.set('/offline', {
      component: 'OfflinePage',
      prefetch: false,
      cache: true
    });

    // Listen for route changes
    window.addEventListener('popstate', (event) => {
      this.navigate(window.location.pathname, false);
    });

    // Intercept link clicks
    document.addEventListener('click', (event) => {
      const link = event.target.closest('a');
      if (link && this.shouldInterceptLink(link)) {
        event.preventDefault();
        this.navigate(link.pathname);
      }
    });
  }

  shouldInterceptLink(link) {
    const href = link.getAttribute('href');

    // Only intercept internal links
    if (!href || href.startsWith('http') || href.startsWith('#')) {
      return false;
    }

    // Don't intercept if modifier keys are pressed
    if (event.ctrlKey || event.metaKey || event.shiftKey) {
      return false;
    }

    // Don't intercept if target is specified
    if (link.target) {
      return false;
    }

    return true;
  }

  async navigate(path, addToHistory = true) {
    // Don't navigate if already on this route
    if (this.currentRoute === path) {
      return;
    }

    try {
      this.showLoading(true);

      // Find route configuration
      const route = this.routes.get(path);

      if (!route) {
        // Try to find dynamic route
        const dynamicRoute = this.findDynamicRoute(path);
        if (dynamicRoute) {
          await this.loadRoute(dynamicRoute, addToHistory, path);
        } else {
          // Fallback to 404 or offline page
          await this.load404Page();
        }
        return;
      }

      await this.loadRoute(route, addToHistory, path);

    } catch (error) {
      console.error('Navigation error:', error);
      await this.handleNavigationError(path);
    } finally {
      this.showLoading(false);
    }
  }

  async loadRoute(route, addToHistory, path) {
    // Update URL
    if (addToHistory) {
      history.pushState({ path: path }, '', path);
    }

    // Preload necessary data
    if (route.preload) {
      await this.preloadData(route.preload);
    }

    // Load and render component
    const component = await this.loadComponent(route.component);
    await this.renderComponent(component);

    // Cache route content
    if (route.cache) {
      await this.cacheRouteContent(path, component);
    }

    // Update current route
    this.currentRoute = path;

    // Update navigation state
    this.updateNavigationState(path);

    // Trigger route change event
    this.dispatchRouteChangeEvent(path);

    // Track page view for analytics
    this.trackPageView(path);
  }

  async loadComponent(componentName) {
    try {
      // Check if component is already loaded
      if (window[componentName]) {
        return window[componentName];
      }

      // Load component script
      const module = await import(`/components/${componentName}.js`);
      window[componentName] = module.default;

      return module.default;

    } catch (error) {
      console.error('Failed to load component:', componentName, error);

      // Fallback to simple content
      return {
        render: () => '<div class="error">Failed to load page content</div>'
      };
    }
  }

  async renderComponent(component) {
    if (typeof component.render === 'function') {
      const html = await component.render();

      // Animate content change
      await this.animateContentChange(() => {
        this.contentElement.innerHTML = html;

        // Initialize component if needed
        if (component.init) {
          component.init();
        }
      });

    } else {
      throw new Error('Component does not have a render method');
    }
  }

  async animateContentChange(callback) {
    // Fade out current content
    this.contentElement.classList.add('fade-out');

    // Wait for animation
    await new Promise(resolve => {
      setTimeout(resolve, 200);
    });

    // Update content
    callback();

    // Fade in new content
    this.contentElement.classList.remove('fade-out');
    this.contentElement.classList.add('fade-in');

    // Wait for animation
    await new Promise(resolve => {
      setTimeout(resolve => 200);
    });

    this.contentElement.classList.remove('fade-in');
  }

  async preloadData(urls) {
    const promises = urls.map(url => this.preloadDataUrl(url));

    try {
      await Promise.allSettled(promises);
    } catch (error) {
      console.error('Error preloading data:', error);
    }
  }

  async preloadDataUrl(url) {
    try {
      // Check cache first
      const cachedResponse = await this.getCachedData(url);
      if (cachedResponse) {
        return cachedResponse;
      }

      // Fetch and cache
      const response = await fetch(url);
      const data = await response.json();

      await this.cacheData(url, data);

      return data;

    } catch (error) {
      console.error('Error preloading data URL:', url, error);
    }
  }

  async cacheData(url, data) {
    try {
      const cache = await caches.open('api-data-cache-v1');
      const response = new Response(JSON.stringify(data), {
        headers: { 'Content-Type': 'application/json' }
      });

      await cache.put(url, response);

    } catch (error) {
      console.error('Error caching data:', error);
    }
  }

  async getCachedData(url) {
    try {
      const cache = await caches.open('api-data-cache-v1');
      const response = await cache.match(url);

      if (response) {
        return await response.json();
      }

      return null;

    } catch (error) {
      console.error('Error getting cached data:', error);
      return null;
    }
  }

  setupNavigation() {
    const navItems = document.querySelectorAll('[data-route]');

    navItems.forEach(item => {
      const route = item.getAttribute('data-route');

      // Set initial state
      if (window.location.pathname === route) {
        item.classList.add('active');
      }

      // Add click handler
      item.addEventListener('click', (event) => {
        event.preventDefault();
        this.navigate(route);
      });
    });

    // Setup mobile navigation
    this.setupMobileNavigation();
  }

  setupMobileNavigation() {
    const menuToggle = document.getElementById('menu-toggle');
    const mobileNav = document.getElementById('mobile-navigation');

    if (menuToggle && mobileNav) {
      menuToggle.addEventListener('click', () => {
        mobileNav.classList.toggle('open');
        document.body.classList.toggle('nav-open');
      });

      // Close nav when clicking outside
      document.addEventListener('click', (event) => {
        if (!mobileNav.contains(event.target) && !menuToggle.contains(event.target)) {
          mobileNav.classList.remove('open');
          document.body.classList.remove('nav-open');
        }
      });
    }
  }

  updateNavigationState(path) {
    // Update active navigation items
    const navItems = document.querySelectorAll('[data-route]');

    navItems.forEach(item => {
      const itemRoute = item.getAttribute('data-route');

      if (path === itemRoute) {
        item.classList.add('active');
      } else {
        item.classList.remove('active');
      }
    });

    // Update page title
    const route = this.routes.get(path) || this.findDynamicRoute(path);
    if (route && route.title) {
      document.title = route.title;
    }

    // Update meta description
    if (route && route.description) {
      const metaDescription = document.querySelector('meta[name="description"]');
      if (metaDescription) {
        metaDescription.content = route.description;
      }
    }
  }

  setupPrefetching() {
    // Prefetch resources on hover
    document.addEventListener('mouseover', (event) => {
      const link = event.target.closest('a[data-route]');

      if (link) {
        const route = link.getAttribute('data-route');
        const routeConfig = this.routes.get(route);

        if (routeConfig && routeConfig.prefetch && !this.isRoutePrefetched(route)) {
          this.prefetchRoute(routeConfig);
          this.markRouteAsPrefetched(route);
        }
      }
    });

    // Prefetch resources on page load for critical routes
    this.prefetchCriticalRoutes();
  }

  async prefetchRoute(route) {
    try {
      // Prefetch component
      await import(`/components/${route.component}.js`);

      // Prefetch data
      if (route.preload) {
        await this.preloadData(route.preload);
      }

    } catch (error) {
      console.error('Error prefetching route:', error);
    }
  }

  prefetchCriticalRoutes() {
    // Prefetch dashboard and other critical routes after initial load
    setTimeout(() => {
      this.routes.forEach((route, path) => {
        if (route.prefetch && path !== this.currentRoute) {
          this.prefetchRoute(route);
        }
      });
    }, 2000);
  }

  isRoutePrefetched(route) {
    return localStorage.getItem('prefetched-' + route) === 'true';
  }

  markRouteAsPrefetched(route) {
    localStorage.setItem('prefetched-' + route, 'true');
  }

  setupHistoryNavigation() {
    // Handle browser back/forward buttons
    window.addEventListener('popstate', (event) => {
      if (event.state && event.state.path) {
        this.navigate(event.state.path, false);
      }
    });

    // Save initial state
    history.replaceState({ path: window.location.pathname }, '');
  }

  handleInitialLoad() {
    // Handle initial page load
    this.currentRoute = window.location.pathname;
    this.updateNavigationState(this.currentRoute);

    // Show content if it exists
    if (this.contentElement.children.length === 0) {
      this.navigate(this.currentRoute, false);
    }
  }

  async cacheRouteContent(path, component) {
    try {
      // Cache component for offline use
      const content = component.render ? component.render() : '';

      const cache = await caches.open('route-cache-v1');
      const response = new Response(content, {
        headers: { 'Content-Type': 'text/html' }
      });

      await cache.put(path, response);

    } catch (error) {
      console.error('Error caching route content:', error);
    }
  }

  findDynamicRoute(path) {
    // Simple dynamic route matching
    for (const [routePath, config] of this.routes.entries()) {
      if (routePath.includes(':')) {
        const pattern = new RegExp('^' + routePath.replace(/:[^/]+/g, '[^/]+') + '$');
        if (pattern.test(path)) {
          return config;
        }
      }
    }
    return null;
  }

  async load404Page() {
    const component = await this.loadComponent('Error404Page');
    component.path = window.location.pathname;
    await this.renderComponent(component);
  }

  async handleNavigationError(path) {
    console.error('Navigation failed for path:', path);

    // Try to load offline page
    try {
      const component = await this.loadComponent('OfflinePage');
      await this.renderComponent(component);
    } catch (error) {
      // Ultimate fallback
      this.contentElement.innerHTML = `
        <div class="error-page">
          <h1>Navigation Error</h1>
          <p>Unable to load the requested page.</p>
          <button onclick="window.location.reload()">Try Again</button>
        </div>
      `;
    }
  }

  setupServiceWorkerComms() {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.addEventListener('message', (event) => {
        const { type, data } = event.data;

        switch (type) {
          case 'cache-updated':
            this.handleCacheUpdate(data);
            break;
          case 'route-available':
            this.handleRouteAvailable(data);
            break;
        }
      });
    }
  }

  handleCacheUpdate(data) {
    // Show update notification if current route was updated
    if (data.url === window.location.pathname) {
      this.showUpdateNotification();
    }
  }

  handleRouteAvailable(data) {
    // Prefetch newly available route
    if (this.routes.has(data.path)) {
      this.prefetchRoute(this.routes.get(data.path));
    }
  }

  showUpdateNotification() {
    const notification = document.createElement('div');
    notification.className = 'update-notification';
    notification.innerHTML = `
      <span>Content updated</span>
      <button onclick="window.location.reload()">Refresh</button>
    `;

    document.body.appendChild(notification);

    setTimeout(() => {
      notification.classList.add('show');
    }, 100);

    setTimeout(() => {
      notification.classList.remove('show');
      setTimeout(() => {
        if (notification.parentNode) {
          document.body.removeChild(notification);
        }
      }, 300);
    }, 5000);
  }

  showLoading(show) {
    if (this.loadingIndicator) {
      this.loadingIndicator.style.display = show ? 'block' : 'none';
    }
  }

  dispatchRouteChangeEvent(path) {
    const event = new CustomEvent('routechange', {
      detail: { path: path }
    });
    window.dispatchEvent(event);
  }

  trackPageView(path) {
    // Analytics tracking
    if (window.gtag) {
      gtag('config', 'GA_MEASUREMENT_ID', {
        page_path: path
      });
    }

    // Track route change for debugging
    console.log('Page view:', path);
  }
}

// Component examples
// HomePage.js
export default {
  render: async () => `
    <div class="home-page">
      <h1>Welcome to My PWA</h1>
      <p>This is a Progressive Web Application with offline capabilities.</p>
      <nav class="quick-actions">
        <a href="/dashboard" data-route="/dashboard">Dashboard</a>
        <a href="/profile" data-route="/profile">Profile</a>
      </nav>
    </div>
  `,

  init: function() {
    console.log('Home page initialized');
  }
};

// DashboardPage.js
export default {
  render: async () => {
    const userData = await window.pwaApp.apiRequest('/api/user-data');

    return `
      <div class="dashboard-page">
        <h1>Dashboard</h1>
        <div class="stats">
          <div class="stat">
            <span class="stat-value">${userData.visits || 0}</span>
            <span class="stat-label">Visits</span>
          </div>
          <div class="stat">
            <span class="stat-value">${userData.tasks || 0}</span>
            <span class="stat-label">Tasks</span>
          </div>
        </div>
      </div>
    `;
  },

  init: function() {
    this.refreshInterval = setInterval(() => {
      window.pwaApp.apiRequest('/api/refresh-stats');
    }, 30000);
  },

  cleanup: function() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
    }
  }
};

// Initialize the app shell
document.addEventListener('DOMContentLoaded', () => {
  window.appShell = new PWAShell();
});