Three days. From nothing to a Progressive Web App that actually installs on my phone.
The project itself is simple by design—a text input that echoes whatever you type into an output box. It’s the kind of trivial app that lets me focus entirely on the PWA mechanics instead of getting lost in application logic. Beyond just learning PWAs, I set some additional constraints: use only HTML, CSS, and vanilla JavaScript whenever possible, and reduce dependency on frameworks. Back to basics.
## Tuesday: Setting Up
Tuesday was mostly about getting the foundations right. Built out the HTML structure using semantic elements—`<main>`, `<header>`, `<section>`—good practice for accessibility and screen readers. Added a manifest link in the head and a theme-color meta tag. First hints this would become more than a web page.
The CSS came together quickly, though I’m working with a knowledge base from around 2005-2010. CSS custom properties (variables) were new to me—they’ve been around since 2016 but I’d never used them. The convenience of changing `–primary-color` in one place and having it cascade everywhere is something I took for granted until I remembered the old days of find-and-replace for color codes. The good news is that browser support has become almost universal now for most modern CSS features, a stark contrast to the fragmentation we dealt with back then. Grid layout for the main container, some nice focus states on the input with a subtle box-shadow, and a status indicator using a `::before` pseudo-element for the colored dot. Clean and app-like.
JavaScript was straightforward: grab references to the DOM elements, add an event listener on the input, update the output text on every keystroke. But then came the interesting part—registering the service worker.
Service worker registration is where PWAs start to differentiate themselves. The code itself is simple—check if `serviceWorker` is supported in the navigator, then call `register(‘sw.js’)`. But behind that simple API is a whole lifecycle: installation, activation, fetch interception. Created a basic service worker file with skeleton event listeners for install, activate, and fetch. No actual caching logic yet, just console logs to verify the lifecycle was working correctly.
Added an online/offline status indicator because it felt important to show connection state once the app could work offline. Using `navigator.onLine` and listening for `online` and `offline` events. The indicator updates immediately when you toggle WiFi. There’s a caveat that `navigator.onLine === true` doesn’t guarantee working internet—just means “connected to a network”—but it’s good enough for a visual indicator.
## Wednesday: Making It Actually Work Offline
Wednesday was all about caching. This is where the PWA magic happens.
Started with the install event in the service worker. When the service worker installs, it opens a cache (named ‘pwa-text-echo-v1′ for versioning), then uses `cache.addAll()` to fetch and store all the static assets: index.html, styles.css, app.js, manifest.json. The `addAll()` method is atomic—if any file fails to cache, the entire operation fails and the service worker won’t activate. That’s both convenient (all-or-nothing is easier to reason about) and frustrating when you have a typo in a URL.
The fetch event is where I intercept network requests. Implemented a cache-first strategy: check if the request is in the cache, return it if found, otherwise fetch from the network. This means after the first visit, everything loads from the cache. Instant load times, and more importantly, works completely offline.
Had to use `event.waitUntil()` to ensure the browser waits for caching to complete before considering the install done. This was an interesting learning moment—service workers run in a separate thread from the main page, so there’s no risk of freezing the UI even if caching takes several seconds. The browser enforces timeouts (around 5 minutes for install/activate events), and if you exceed them, it kills the service worker and retries later.
Added cleanup logic in the activate event. When a new service worker activates, it checks for old caches (anything that doesn’t match the current cache name) and deletes them. Important for preventing storage bloat. Increment the cache version, deploy, and the activate event cleans up the old cache automatically.
Testing in DevTools was enlightening. The Application tab has a dedicated Service Workers panel where I could see the registration status, manually update, unregister, and—most useful during development—check “Update on reload” to force the service worker to reinstall every time I refresh. Without that checkbox, I was constantly fighting stale service workers serving old cached content.
## Thursday: Installation and The Path Issues
Thursday started well. Added the install button functionality using the `beforeinstallprompt` event. The event fires when the browser determines the app is installable (all the criteria met: HTTPS, valid manifest, registered service worker, etc.). The code captures the event, prevents the default browser prompt, stores it in a variable, and shows a custom install button. When clicked, it calls `deferredPrompt.prompt()` to trigger the browser’s native install dialog.
Created the app icons—192×192 and 512×512 PNG files with the 📱 emoji as a placeholder. These go in an `icons` folder and get referenced in the manifest. Icons need to be cached too, so added them to the `FILES_TO_CACHE` array in the service worker.
Then I hit the deployment snag.
Deployed to GitHub Pages, which gives you HTTPS for free (essential for PWAs). The URL format is `https://username.github.io/demo-pwa-app/`—note the subdirectory. And this is where absolute vs relative paths became a problem.
In the code, I’d used absolute paths like `/icons/icon-192×192.png` and `/index.html`. On localhost, these resolve correctly: `http://localhost:5500/icons/icon-192×192.png`. But on GitHub Pages, the absolute path resolves to `https://vitorsilva.github.io/icons/icon-192×192.png`—missing the `/demo-pwa-app/` part. So all the icons returned 404s.
The fix was changing all absolute paths to relative paths: `icons/icon-192×192.png` instead of `/icons/icon-192×192.png`. Same for the manifest’s `start_url` and `scope` properties—changed from `/` to `./`. And the service worker registration needed to be relative too: `register(‘sw.js’)` instead of `register(‘/sw.js’)`.
This spawned the series of “fix url” commits you see in the history. Each time I thought I’d fixed it, I’d discover another absolute path somewhere else. The service worker was caching files at the wrong URLs. The manifest icons weren’t loading. The installed app opened to a 404 because `start_url` pointed to the domain root instead of the app subdirectory.
Eventually got them all. The trick was incrementing the cache version number each time—forced the service worker to reinstall and cache files at the new paths. By the afternoon, everything was working correctly on GitHub Pages.
Tested on my Android phone. Opened Chrome, navigated to the deployed URL, saw the install button, tapped it, got the browser’s install dialog, accepted. The app appeared on my home screen with the icon. Tapped to launch, and it opened in standalone mode—no browser UI, just the app. Turned off WiFi, and it still worked perfectly. Typed in the input, saw the output update, watched the status indicator change from green “Online” to red “Offline”.
## What I Learned About Service Workers
Service workers are conceptually elegant but have sharp edges in practice. The lifecycle is well-designed—install, activate, fetch, idle—but the timing tripped me up. Change the service worker file, refresh the page, and… nothing happens. The old service worker is still active, still serving cached content. I needed to either close all tabs and reopen, or use `skipWaiting()` to force the new service worker to activate immediately.
During development, this is annoying. In production, it’s actually a feature—you don’t want to swap out service workers mid-session while users are actively using the app. But it means I needed to be thoughtful about updates. I used `skipWaiting()` in the install event for this learning project to get fast iterations, but in a real app, I’d probably want to prompt the user first.
The `event.waitUntil()` pattern is interesting. It tells the browser “don’t consider this event complete until this Promise resolves.” Without it, the browser might think installation finished before files are cached. With it, you get guaranteed atomicity—the service worker only activates if caching succeeds.
Caching strategy matters more than I initially thought. Cache-first makes the app blazingly fast after the first visit—everything loads instantly from the cache. But it also means I’m serving stale content. If I update my CSS, users won’t see changes unless I increment the cache version or implement a more sophisticated strategy like stale-while-revalidate (serve from cache immediately, fetch fresh version in background, update cache for next time).
For this project, cache-first with versioned caches works fine. Change something, bump the version number, the activate event cleans up the old cache. Simple and predictable. But for a real app with frequent updates, I’d want something smarter, probably Workbox.
## The Install Prompt Dance
The `beforeinstallprompt` event is clever but platform-dependent. On Chrome and Edge, it fires when the app meets installability criteria, and I could capture it to show a custom install button. On Firefox, the browser adds a small icon to the address bar automatically—no JavaScript API. On iOS Safari, users manually tap Share → Add to Home Screen—no programmatic control at all.
This is progressive enhancement in action. Write code that works on Chromium browsers, and the app is still installable everywhere else through browser-native UI. The install button is an enhancement, not a requirement.
One subtlety: after showing the prompt, I can’t show it again in the same browser session. So if the user declines, I hide the button. Otherwise it would be clickable but do nothing. The event only fires once per session. This makes sense from a UX perspective—you don’t want to spam users—but it means I need to be thoughtful about when to show the button.
The `appinstalled` event fires when the user actually installs the app. Good for analytics, but I didn’t do much with it beyond logging. In a real app, I’d probably track install events to measure conversion rates.
## Debugging in DevTools
I really like what Google DevTools has to offer for PWA development. Chrome DevTools’ Application tab is purpose-built for PWAs, providing complete debugging tools that go way beyond just console logging. The Service Workers panel shows registration status, lets you manually update or unregister, and has an “Offline” checkbox to simulate being offline. The Cache Storage section shows exactly what’s cached—you can inspect individual entries, see response headers, delete caches. The Manifest panel validates your manifest and shows icon previews.
Most useful during development: the “Update on reload” checkbox in Service Workers. Without it, I was constantly fighting stale service workers. With it, every refresh installs the latest version. Huge time saver.
I used console logs liberally in the service worker—tagged with `[SW]` prefix to distinguish them from main page logs. Every install, activate, and fetch event logged what it was doing. Made debugging much easier. Breakpoints work in service workers too, though I found logs more useful for understanding the flow.
The fetch event fires for *every* network request—HTML, CSS, JS, images, fonts, everything. Setting a breakpoint there will pause constantly. Better to use conditional breakpoints or just console logs.
## Path Resolution Lessons
The absolute vs relative path problem on GitHub Pages was frustrating but educational. It’s one of those things that works fine in development (where my app is at the domain root) and breaks in production (where it’s in a subdirectory).
The fix is always using relative paths for anything that needs to work in both environments. Manifests, service workers, icon references, all of it. Using `./` is more explicit than just omitting the leading slash, so I went with that.
Incrementing the cache version after path changes is critical. Without it, the service worker keeps serving files from the old paths, and I was left confused why my fixes weren’t working. Bump the version, the service worker reinstalls with the new paths, the activate event cleans up the old cache. Clean slate.
## Real Device Testing
Testing on an actual Android phone revealed things DevTools doesn’t catch. The install flow is different—the browser banner appears at the bottom, you tap Install, the system installs it, the icon appears in your app drawer and on your home screen. The launch experience is different—standalone mode really does feel like a native app. No browser chrome, no address bar, just your app.
The offline experience is smoother on the device. Toggle airplane mode, and the status indicator updates immediately. The app keeps working. Type in the input, see the output. It’s such a simple interaction, but it drives home the point that this is a real offline-capable app.
Uninstalling is straightforward—when you open the installed PWA, click the three dots in the top right corner and select “Uninstall”. Useful for testing the install flow multiple times.
I didn’t test on iOS since I don’t have an iPhone handy.
—
**Project**: demo-pwa-app
**Week**: week #1
**Commits**: 16
**Status**: Phase 3 complete—deployed, installable, tested on mobile
**Lines added**: ~5,200 (including extensive learning notes documenting every question and answer)

Deixe um comentário