Service workers, manifest file, offline support — a beginner-friendly, fastest tutorial to build a Progressive Web App (PWA) you can install and test in ~15 minutes. No frameworks required: plain HTML, CSS and JavaScript.
Info!
This guide focuses on a minimal, practical PWA that adds a manifest, registers a service worker, caches assets, supports offline fallback, and prompts install — all in one small project. Follow the exact steps and you'll have a working PWA in about 15 minutes.
What you'll build — TL;DR
We’ll create a tiny app with:
- index.html (UI + registration code)
- manifest.json (app metadata)
- service-worker.js (caching + offline)
- icons (few sizes) and HTTPS friendly hosting
Result: a site that loads instantly from cache, works offline, and can be installed on phones & desktops as a PWA.
Why PWAs matter (short)
Progressive Web Apps combine best of web + native: fast load, offline capability, installable, and engageable via push notifications. They improve retention and conversion without app stores.
Tip:
You don’t need a build tool to do this tutorial — plain files and a static host (Vercel, Netlify, GitHub Pages) are enough. For local testing, use a simple static server (instructions below).
Project structure (what files you’ll create)
pwa-demo/ ├─ index.html ├─ styles.css ├─ app.js ├─ manifest.json ├─ service-worker.js └─ icons/ ├─ icon-192.png └─ icon-512.png Step 0 — prerequisites
- Basic HTML/CSS/JS knowledge
- Node.js (optional, for a local static server) or Python
- HTTPS host (Netlify/Vercel/GitHub Pages) for production PWA — for local testing use
http://localhost
Step 1 — Create a minimal index.html
Create index.html with UI + manifest link + service worker registration. Paste this exact file:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>PWA Demo — MaxonCodes</title> <link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#0ea5a4"> <link rel="icon" href="/icons/icon-192.png"> <link rel="stylesheet" href="/styles.css"> </head> <body> <main class="app"> <h1>PWA Demo</h1> <p>This is a simple Progressive Web App example — works offline and installable.</p> <button id="btn-refresh">Refresh cache & update</button> <p id="status">Status: <em>online</em></p> </main> <script src="/app.js"></script> </body> </html> Info!
Make sure your manifest link path is correct. If you serve from a subfolder, adjust paths accordingly.
Step 2 — Add a basic manifest.json
Create manifest.json at the site root — this tells the browser about your app and enables install prompt.
{ "name": "PWA Demo - MaxonCodes", "short_name": "PWA Demo", "description": "A tiny Progressive Web App demo with offline support.", "start_url": "/", "scope": "/", "display": "standalone", "background_color": "#0ea5a4", "theme_color": "#0ea5a4", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } Icons: include at least 192×192 and 512×512 PNGs. You can generate them from a single large image with tools like ImageMagick or online favicon generators.
Step 3 — Create a minimal stylesheet (styles.css)
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;display:flex;min-height:100vh;align-items:center;justify-content:center;background:#f8fafc;color:#0f172a;margin:0} .app{max-width:720px;padding:24px;border-radius:12px;background:#ffffff;box-shadow:0 8px 30px rgba(2,6,23,0.08);text-align:center} button{background:#0ea5a4;color:#fff;padding:10px 16px;border:none;border-radius:8px;cursor:pointer} #status em{color:#0ea5a4;font-weight:600} Step 4 — Add service-worker.js (cache assets + offline fallback)
Create service-worker.js at the root. This service worker implements a simple cache-first strategy and an offline fallback page.
const CACHE_NAME = "pwa-demo-v1"; const ASSETS = [ "/", "/index.html", "/styles.css", "/app.js", "/icons/icon-192.png", "/icons/icon-512.png" ]; // Install: cache essential assets self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) ); self.skipWaiting(); }); // Activate: clean 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); }) )) ); self.clients.claim(); }); // Fetch: cache-first strategy with fallback self.addEventListener("fetch", (event) => { if (event.request.method !== "GET") return; event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; return fetch(event.request).then((response) => { // Optionally cache new requests return caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, response.clone()); return response; }); }).catch(() => caches.match("/index.html")); // offline fallback }) ); }); Tip:
This is a minimal example. For production, consider runtime caching, stale-while-revalidate, and caching only for same-origin assets to avoid caching third-party responses inadvertently.
Step 5 — Register the service worker & handle install prompt (app.js)
Create app.js to register the service worker, show status, and handle manual cache update.
const statusEl = document.getElementById("status"); const btnUpdate = document.getElementById("btn-refresh"); if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/service-worker.js") .then((reg) => { console.log("SW registered", reg); statusEl.innerHTML = 'Status: service worker registered'; }) .catch((err) => { console.error("SW registration failed", err); statusEl.innerHTML = 'Status: sw registration failed'; }); } else { statusEl.innerHTML = 'Status: service worker not supported'; } // Optional: simple "update" button that prompts new service worker to take control btnUpdate.addEventListener("click", async () => { if (navigator.serviceWorker.controller) { statusEl.innerHTML = 'Status: Updating cache...'; const reg = await navigator.serviceWorker.getRegistration(); if (reg && reg.waiting) { reg.waiting.postMessage({ action: "skipWaiting" }); } else if (reg) { reg.update(); statusEl.innerHTML = 'Status: Checked for update'; } } }); // Listen for online/offline window.addEventListener("online", () => statusEl.querySelector("em").textContent = "online"); window.addEventListener("offline", () => statusEl.querySelector("em").textContent = "offline"); Step 6 — Add messaging to service worker for skipWaiting
To allow immediate activation when a new SW is installed, add a message listener in service-worker.js (append):
// inside service-worker.js self.addEventListener("message", (event) => { if (event.data && event.data.action === "skipWaiting") { self.skipWaiting(); } }); Step 7 — Add icons
Create an icons/ folder with at least:
icon-192.png(192×192)icon-512.png(512×512)
Use a square image, exported to PNG. There are online generators (e.g., RealFaviconGenerator) that create all required sizes and HTML snippets.
Warning!
PWAs require HTTPS in production — except on localhost. Deploy to a secure host when you go live (Netlify, Vercel, Cloudflare Pages, GitHub Pages with HTTPS enabled).
Step 8 — Test locally (quick)
Run a local static server and visit http://localhost:
# Using npm http-server (quick) npx http-server -c-1 -p 8080 # Or Python 3 python -m http.server 8080 Open DevTools → Application → Service Workers to confirm registration. Use Lighthouse (Audits) to run a PWA audit.
Step 9 — Deploy to production (fast options)
Recommended free/fast hosts:
- Vercel: connect repo, push to main — automatic HTTPS & CDN
- Netlify: drag-and-drop or connect Git repo — automatic HTTPS
- GitHub Pages: use with
gh-pagesor static export (ensure HTTPS)
Once deployed over HTTPS, visiting the site on mobile should prompt an install banner (or the browser will show “Install app” in the address bar / menu).
Step 10 — Verify PWA & Core Web Vitals
Open Chrome DevTools → Lighthouse → run an audit (PWA, Performance). Key checks:
- Service worker registered
- Has a valid manifest
- Is installable
- Works offline (controlled by service worker)
Optional Enhancements (go beyond 15 minutes)
- Use Workbox for advanced caching strategies (precache + runtime caching + routing)
- Implement stale-while-revalidate or network-first for API responses
- Add Push Notifications (requires server-side subscription handling)
- Use IndexedDB for structured offline data storage
- Implement background sync to retry failed requests
Troubleshooting (quick)
| Problem | Fix |
|---|---|
| Service worker not registering | Check console for errors, ensure service-worker.js is served from root and site is on localhost or HTTPS. |
| Install prompt not showing | Ensure manifest exists, icons valid, page served over HTTPS and service worker is active. |
| Offline page shows blank | Make sure that index.html is cached and that your fetch fallback returns cached page. |
How does caching strategy affect updates?
If you use cache-first, users might see stale content until new service worker activates. Use versioned cache names, call skipWaiting() on install, and prompt the user to refresh when a new SW is available.
SEO & PWA best practices
- Ensure each page has unique
<title>and<meta name="description"> - Provide structured data (Article/Product) as JSON-LD
- Pre-render important pages and use server-side rendering for SEO-critical content if necessary
- Keep the manifest updated and provide accurate start_url and scope
Should every website be a PWA?
Not necessarily. PWAs are highly useful for sites with repeat visitors, mobile-focused audiences, or apps requiring offline/installation. For single-visit brochure sites, it may not be worth the extra work.
FAQs
1. What is the minimum requirement for a PWA?
At minimum: a valid manifest.json, a registered service worker, served over HTTPS (or localhost for dev), and at least one icon.
2. Do PWAs work on iOS?
Yes — iOS supports many PWA features via “Add to Home Screen,” but service worker and some APIs have limitations. iOS Safari requires specific meta tags (apple-touch-icon) and doesn't always show the install prompt.
3. How do I update cached assets?
Use cache versioning: change CACHE_NAME. On activate, remove old caches. Or implement a strategy to revalidate and update caches (Workbox helps a lot).
4. Can I add push notifications?
Yes — but push requires a backend to manage subscriptions and send push messages via Push API (VAPID keys). This is an advanced feature beyond this 15-minute tutorial.
5. How can I make my PWA installable on desktop?
Desktop Chrome shows an install button in the address bar when manifest + service worker + HTTPS are present. You can also use the beforeinstallprompt event to control when to show your own install CTA.
Credit:
Step-by-step PWA tutorial by MaxonCodes.com. Use this as a base for learning or production — customize caching and data strategies for real apps.