Website today.
Web app for life.
A practical path from an ordinary website to a Progressive Web App (PWA): give it an app identity, design for unreliable connections, and keep it fresh as the website evolves.
What makes it a PWA?
A usable website
Start with readable content, responsive layouts, accessible controls and working URLs. Visitors should still be able to use the site in a browser.
An installable experience
A manifest describes the name, icons, launch URL and display mode. Serve over HTTPS; localhost is suitable for development. Installation behaviour varies across browsers.
Deliberate offline support
A service worker can serve saved resources when the network fails. Installation alone does not make a site work offline; these are separate capabilities.
Reference: MDN · Making PWAs installable.
From page to app
flowchart TD
accTitle: Website to PWA build roadmap
accDescr: Improve the website, serve securely, add identity, choose offline behaviour, then test and release.
A["Existing website"] --> B["Check mobile layout, accessibility and links"]
B --> C["Serve over HTTPS"]
C --> D["Add manifest and app icons"]
D --> E["Link manifest from app pages"]
E --> F["Define offline content and fallback"]
F --> G["Register a scoped service worker"]
G --> H["Test installation, offline use and updates"]
H --> I{"Checks pass?"}
I -- No --> F
I -- Yes --> J["Release and maintain"]View Mermaid source
flowchart TD
accTitle: Website to PWA build roadmap
accDescr: Improve the website, serve securely, add identity, choose offline behaviour, then test and release.
A["Existing website"] --> B["Check mobile layout, accessibility and links"]
B --> C["Serve over HTTPS"]
C --> D["Add manifest and app icons"]
D --> E["Link manifest from app pages"]
E --> F["Define offline content and fallback"]
F --> G["Register a scoped service worker"]
G --> H["Test installation, offline use and updates"]
H --> I{"Checks pass?"}
I -- No --> F
I -- Yes --> J["Release and maintain"]Give the app a home
Example implementation plan: create a separate field-notes/ folder. The snippets below are learning examples; this guide itself does not register a service worker.
field-notes/
index.html # App entry page
manifest.webmanifest # App identity
sw.js # Offline request handling
offline.html # Self-contained offline message
icons/
icon-192.png # Actual 192 × 192 PNG
icon-512.png # Actual 512 × 512 PNG{
"id": "./",
"name": "Flying Fox Field Notes",
"short_name": "Field Notes",
"start_url": "./index.html",
"scope": "./",
"display": "standalone",
"background_color": "#161618",
"theme_color": "#161618",
"icons": [
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}Keep these relative URLs under the same app folder, including when hosting under a repository subpath. Supply actual icon files at the declared sizes. Link the manifest in every page belonging to the app.
<link rel="manifest" href="./manifest.webmanifest">
<meta name="theme-color" content="#161618"><script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js")
.catch(error => console.error("Service worker registration failed:", error));
});
}
</script>The manifest scope describes app navigation; service worker scope controls which pages it can intercept. They are separate settings. See MDN · Using service workers.
Plan for the missing connection
flowchart TD
accTitle: Network-first navigation with an offline fallback
accDescr: An active worker attempts an online page request. On network failure it returns a precached offline page.
A["Navigation in a controlled app page"] --> B["Active service worker"]
B --> C["Try the network"]
C --> D{"Network response received?"}
D -- Yes --> E["Return server response"]
D -- No --> F["Read precached offline.html"]
F --> G["Show offline message"]View Mermaid source
flowchart TD
accTitle: Network-first navigation with an offline fallback
accDescr: An active worker attempts an online page request. On network failure it returns a precached offline page.
A["Navigation in a controlled app page"] --> B["Active service worker"]
B --> C["Try the network"]
C --> D{"Network response received?"}
D -- Yes --> E["Return server response"]
D -- No --> F["Read precached offline.html"]
F --> G["Show offline message"]const CACHE = "ff-field-notes-offline-v1";
const OFFLINE = new URL("./offline.html", self.location).href;
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.add(OFFLINE))
);
});
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(key => key.startsWith("ff-field-notes-offline-") && key !== CACHE)
.map(key => caches.delete(key))
))
);
});
self.addEventListener("fetch", event => {
if (event.request.mode !== "navigate" || event.request.method !== "GET") return;
event.respondWith(
fetch(event.request).catch(async () => {
const cache = await caches.open(CACHE);
return (await cache.match(OFFLINE)) || new Response(
"You are offline. Reconnect and try again.",
{ status: 503, headers: { "Content-Type": "text/plain; charset=utf-8" } }
);
})
);
});Create offline.html with inline styles and a clear reconnect message so it needs no external assets. Load the app online first, wait for worker activation, then reload to enter worker control before testing offline. A server error such as HTTP 500 is still a network response; handle that separately if needed.
| Resource | Useful starting strategy | Tradeoff |
|---|---|---|
| HTML / changing content | Network first, with a cached fallback | Fresh online; consider a timeout for slow networks. |
| Versioned CSS, JS, icons | Cache first | Fast; change asset URLs when their contents change. |
| Public, frequently reused content | Stale while revalidate | Fast cached result; refresh in the background. |
| Private data / submissions | Explicit application policy | Avoid generic caching; define logout cleanup and retry behaviour. |
Reference: MDN · Caching strategies. Offline diagrams and third-party fonts also need a deliberate asset-caching plan.
Ship the next version carefully
flowchart TD
accTitle: Service worker update lifecycle
accDescr: A changed worker installs and waits while the previous worker controls pages, then activates and removes old app caches.
A["Browser detects changed sw.js"] --> B["Install new worker and prepare cache"]
B --> C{"Installation succeeds?"}
C -- No --> D["Existing worker remains active"]
C -- Yes --> E["Wait while old worker controls pages"]
E --> F["Old controlled tabs close"]
F --> G["Activate new worker"]
G --> H["Remove this app's old caches"]
H --> I["Next app visit uses new worker"]View Mermaid source
flowchart TD
accTitle: Service worker update lifecycle
accDescr: A changed worker installs and waits while the previous worker controls pages, then activates and removes old app caches.
A["Browser detects changed sw.js"] --> B["Install new worker and prepare cache"]
B --> C{"Installation succeeds?"}
C -- No --> D["Existing worker remains active"]
C -- Yes --> E["Wait while old worker controls pages"]
E --> F["Old controlled tabs close"]
F --> G["Activate new worker"]
G --> H["Remove this app's old caches"]
H --> I["Next app visit uses new worker"]For the example, increment the cache version when changing the fallback. Keep the worker URL stable, let browsers revalidate it, and remove only caches owned by this app. Test an update with two tabs open before releasing it.
Reference: MDN · Installation and updates.
Prove the experience
Inspect the basics
Use HTTPS or localhost. Check the browser’s Application tools for manifest errors, icon loading, start URL and worker scope.
Install on devices
Verify the launch icon, standalone window and navigation. Installation controls vary; iOS offers Add to Home Screen through the Share menu.
Go offline
After worker control, disable the network and navigate. Confirm the fallback appears. Also test a first visit with no cache and a slow connection.
Upgrade & recover
Deploy a changed worker, test the waiting state, close app tabs and reopen. Verify cleanup and recovery after clearing site data.
Use keyboard navigation and a narrow mobile viewport as part of the same review. Treat offline storage as disposable: browsers or users may clear it. Add an in-page install button only when the browser exposes a supported installation flow.
Installation reference: MDN · Browser installation behaviour.