âš ī¸ Warning âš ī¸ Deprecated Code! This video tutorial contains outdated code.
💡 If you wish to update it, any AI assistant will update the code for you in seconds.

PWA Make Your Website a Native App on Mobile Devices and Desktop

Published : December 8, 2025   •   Last Edited : December 8, 2025   •   Author : Adam Khoury

Learn how to transform any modern website into an installable, app-like experience with this comprehensive Progressive Web App (PWA) crash course! Progressive Web Apps combine the best of web and native applications, offering reliability, speed, and engagement without the need for a traditional app store. If you're looking to provide a seamless user experience and get your website icon right on your user's home screen, this is the tutorial for you.

What You'll Learn:

Understanding PWA Fundamentals: Grasp the core concepts, benefits, and requirements of Progressive Web Apps. The Web Manifest File: Learn to configure the crucial manifest.json file to define your app's appearance, orientation, and how it installs on a user's device. Service Workers: The backbone of PWAs, enabling offline capabilities and background synchronization. Caching Strategies: Implement effective caching strategies (e.g., Cache-first, Network-first) to ensure your app is fast and reliable, even in poor network conditions. Making it Installable (A2HS): Understand the Add to Home Screen (A2HS) prompt and the criteria browsers use to make a PWA installable. Developer Tools and Debugging: Use browser developer tools to inspect and debug your Service Worker and PWA setup. Who is this Video For? This crash course is perfect for web developers, front-end engineers, and digital marketers who want to: * Boost performance and engagement on their websites. * Reduce friction by allowing users to install the website directly. * Provide an offline-first experience. * Learn modern web development techniques. This article here on the site discusses prerequisites and qualifiers: ( opens in new tab ) PWA Implementation Prerequisites and Recommendations site.webmanifest - goes in your root folder
{
  "id": "/index.php?v=3",
  "name": "Adam Khoury",
  "short_name": "AKapp",
  "start_url": "/index.php",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#222222",
  "background_color": "#222222",
  "display": "standalone",
  "screenshots": [
    {
      "src": "/images/homepage-wide.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/images/homepage-mobile.png",
      "sizes": "720x1280",
      "type": "image/png"
    }
  ]
}
sw.js - goes in your root folder
// Service Worker Versioning
const STATIC_CACHE = 'static-v3'; // Increment to force all users to fetch new assets ( ie. 2.1 )
const DYNAMIC_CACHE = 'dynamic-v3'; // Increment to force all users to fetch new assets

// List of core assets to be cached on install
const CORE_ASSETS = [
    '/',
    '/index.php', // Assuming this is your main page file
    '/css/core10.css',
    '/js/core7.js',
    '/android-chrome-192x192.png',
    '/android-chrome-512x512.png',
    '/offline.html'
];

// Add a list of dynamic/network-first URLs
const NETWORK_FIRST_URLS = [
    '/', // Catches the root path request
    '/index.php'
];

// 1. Install Event
self.addEventListener('install', (event) => {
    // Wait until static assets are fully cached before the worker is installed
    event.waitUntil(
        caches.open(STATIC_CACHE)
            .then(cache => cache.addAll(CORE_ASSETS))
            .then(() => self.skipWaiting()) // Force the new SW to activate immediately
            .catch(error => console.error('Static cache installation failed:', error))
    );
});

// 2. Activate Event
self.addEventListener('activate', (event) => {
    // Clean up old caches
    event.waitUntil(
        caches.keys().then(keys => {
            return Promise.all(
                keys.filter(key => key !== STATIC_CACHE && key !== DYNAMIC_CACHE)
                    .map(key => caches.delete(key))
            );
        }).then(() => self.clients.claim()) // Claim clients to control pages immediately
    );
});

// Fetch event - Uses a SPLIT STRATEGY based on URL path:
// 1. Network-First for dynamic pages (e.g., index, videos) to ensure fresh content.
// 2. Cache-First for all other assets (e.g., images, CSS, JS) for performance.
self.addEventListener('fetch', (event) => {
    // Only process GET requests
    if (event.request.method !== 'GET') {
        return;
    }

    const requestURL = new URL(event.request.url).pathname;
    
    // --- 1. Network-First Strategy for Dynamic Pages ---
    if (NETWORK_FIRST_URLS.includes(requestURL)) {
        event.respondWith(
            fetch(event.request)
                .then(networkResponse => {
                    // Check validity and cache the FRESH response
                    if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
                        const responseToCache = networkResponse.clone();
                        caches.open(DYNAMIC_CACHE).then(cache => {
                            cache.put(event.request, responseToCache);
                        });
                    }
                    return networkResponse;
                })
                .catch(() => {
                    // If network fails (offline), fall back to the dynamic cache for the page
                    return caches.match(event.request).then(cachedResponse => {
                        return cachedResponse || caches.match('/offline.html');
                    });
                })
        );
        return; 
    }
    
    // --- 2. Cache-First Strategy for all other Assets (Images, etc.) ---
    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            // Serve from cache if available
            if (cachedResponse) {
                return cachedResponse;
            }

            // Not in cache, proceed to network request
            return fetch(event.request)
                .then(networkResponse => {
                    // Check for invalid responses (e.g., bad status, cross-origin/opaque)
                    if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'opaque') {
                        return networkResponse;
                    }

                    // Clone response and cache it
                    const responseToCache = networkResponse.clone();
                    
                    caches.open(DYNAMIC_CACHE)
                        .then(cache => {
                            cache.put(event.request, responseToCache);
                        });

                    return networkResponse;
                })
                .catch(() => {
                    // If network fails here, check for a static offline file if needed
                    return caches.match('/offline.html');
                });
        })
    );
});
beforeinstallprompt - goes in your site JavaScript code
document.addEventListener('DOMContentLoaded', () => {
  const installbtn = document.getElementById('install_btn');
  let deferredPrompt;

  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    installbtn.style.display = 'inline';
  });

  installbtn.addEventListener('click', async () => {
    if (deferredPrompt) {
      deferredPrompt.prompt();
      const { outcome } = await deferredPrompt.userChoice;
      // console.log(`User response: ${outcome}`);
      deferredPrompt = null;
      installbtn.style.display = 'none';
    }
  });

  window.addEventListener('appinstalled', () => {

  });
}); 
Register your Service Worker - goes in your head element
<link rel="manifest" href="/site.webmanifest?v=3">
<script>
  if ("serviceWorker" in navigator) {
    window.addEventListener("load", () => {
      navigator.serviceWorker.register("/sw.js")
        .then((registration) => {
          console.log("ServiceWorker registered with scope:", registration.scope);
        })
        .catch((error) => {
          console.log("ServiceWorker registration failed:", error);
        });
    });
  }
</script>
Unregister a service worker - goes in your head element ( when you want to remove the service worker and PWA features )
<script>
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistrations().then(registrations => {
    for (let registration of registrations) {
      registration.unregister();
    }
  });
}
</script>