Progressive Web App (PWA) implementation guide: turn your website into an installable app

Many site owners have seen this pattern: a first-time visitor lands on a great article, wants to come back, and has to dig through browser history or retype the URL. PWA (Progressive Web App) solves exactly that — letting an ordinary web page be "installed" onto a desktop or home screen with its own icon and window, and even keep browsing cached content offline. All it takes is one manifest file and one Service Worker.

Using a content site as an example, this article walks through the full journey from configuration to launch. The entire code is under two hundred lines, yet it measurably improves return visits and retention.

Three core components, all required

PWA is called "progressive" because it builds on capabilities the web already has; each component is a progressive enhancement and works independently:

  1. manifest.json — describes the app name, icons, start URL, theme color, and display mode; determines what the app looks like once installed;
  2. Service Worker — JavaScript that runs separately from the page, handling offline caching, request interception, push notifications, and background sync;
  3. HTTPS — a hard requirement. A Service Worker can only register in a secure context, so the first step is always getting HTTPS right; see this guide on certificate types.

An easily missed point: only when manifest, Service Worker, and HTTPS are all in place will Chrome and Edge show the full "Install app" prompt in the address bar. With just a manifest, users can still add the site to their home screen via the menu, but the experience is noticeably weaker.

Step 1: Configure manifest.json

The manifest usually lives in the site root. Using 16IDC as an example:

{
  "name": "16IDC",
  "short_name": "16IDC",
  "description": "Cloud service comparison and website building guide",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4F46E5",
  "lang": "en",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

A few fields are worth attention: scope limits the range the Service Worker can control and defaults to the manifest directory; display: standalone runs the app in its own window without a browser address bar; purpose: "any maskable" is for Android adaptive icons, which need safe padding around the artwork or they get cropped. Set start_url to an in-site relative path to avoid some browsers opening the wrong page after install.

Then reference the manifest in the <head> and add the iOS-only apple-touch-icon:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4F46E5">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">

Without that last line, "Add to Home Screen" on iPhone produces a screenshot thumbnail instead of a proper icon.

Step 2: Register the Service Worker

Register sw.js after the page finishes loading:

// main.js
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const registration = await navigator.serviceWorker.register('/sw.js');
            console.log('ServiceWorker registered:', registration.scope);
        } catch (error) {
            console.log('ServiceWorker registration failed:', error);
        }
    });
}

Put the registration inside the load event so it does not compete with first-paint rendering. For framework and tooling choices, see the JavaScript framework comparison; also verify browser support for the APIs you plan to use.

Step 3: Service Worker caching strategies

A caching strategy decides whether to read from cache or hit the network first; there is no one-size-fits-all answer. The four mainstream strategies:

Strategy Request flow Use case Pros Cons
Cache First Check cache, fall back to network, then refill Versioned CSS, images, static assets Instant load, saves bandwidth Delayed updates
Network First Try network, fall back to cache on failure/timeout Article pages, news feeds Fresh content Slower on weak networks
Stale While Revalidate Return cached, update in background Sidebars, config-like data Instant + eventually consistent Slightly more logic
Network Only Always hit the network Payments, form submissions Data never stale No offline capability

A complete sw.js skeleton (install, activate, and fetch interception):

// sw.js
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
    '/',
    '/styles.css',
    '/app.js',
    '/offline.html'
];

// Install: precache static assets
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(STATIC_ASSETS))
            .then(() => self.skipWaiting())
    );
});

// Activate: clean up old caches
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys =>
            Promise.all(keys.map(key => {
                if (key !== CACHE_NAME) return caches.delete(key);
            }))
        ).then(() => self.clients.claim())
    );
});

// Fetch: cache first, fall back to the offline page
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(cached => cached || fetch(event.request))
            .catch(() => caches.match('/offline.html'))
    );
});

Offline page and update mechanism

Once offline.html is in the precache list, users who lose connectivity and hit an uncached page are routed to the offline page instead of the browser's default error screen. It usually has a logo, a friendly "you're offline" message, and a retry button:

<!DOCTYPE html>
<html>
<head>
    <title>You are offline</title>
    <style>
        body { text-align: center; padding: 50px; font-family: system-ui; }
    </style>
</head>
<body>
    <h1>No network connection</h1>
    <p>Check your connection and try again</p>
</body>
</html>

A common gotcha is "the update didn't apply": the Service Worker calls skipWaiting on install, but already-open pages only switch to the new version on their next load. For an immediate refresh, listen for the controllerchange event on the page and prompt the user when a new version is detected.

Push notifications and tooling

Push notifications are the part of PWA most dependent on third-party services: you need VAPID keys, send the subscription to your backend, and have the backend deliver messages via the Web Push protocol, which is tightly coupled to platform push services like FCM and APNs. If offline support is all you need, this can safely be deferred. Before launch, run a health check:

  • Lighthouse — the Chrome DevTools PWA audit
  • PWABuilder — Microsoft's PWA packaging tool; produces Windows/macOS installers
  • PWA Checker — online PWA compliance checker

FAQ

  • Does it have to be HTTPS? Yes. A Service Worker can only register on HTTPS or localhost; browsers enforce this strictly.
  • What icon sizes do I need? At least 192x192 and 512x512, plus a 180x180 apple-touch-icon for iOS.
  • Does it hurt SEO? No. A PWA is still a regular web page, and good caching actually speeds up loading and improves experience signals.
  • Can it be published to app stores? Yes. PWABuilder packages desktop installers, and Android builds can be published to Google Play via Trusted Web Activity.

16IDC Takeaway

Low implementation cost (one manifest plus one Service Worker, finished in a day or two) and obvious returns (install rate, return visits, weak-network experience), PWA is among the highest-ROI experience optimizations for content and tool sites. Pair it with the performance optimization guide to squeeze out more.

Reference: Mozilla Web docs — https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps ; Google Web Dev — https://web.dev/learn/pwa