-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add PWA support for Android and iOS installation #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # Implementation Plan: Progressive Web App (PWA) Support | ||
|
|
||
| > **Spec ID:** 028-pwa | ||
| > **Status:** In Progress | ||
| > **Last Updated:** 2026-05-27 | ||
| > **Estimated Effort:** S | ||
|
|
||
| ## Summary | ||
|
|
||
| Add a web app manifest, minimal service worker, and PWA meta tags to make ROTV installable on Android and iOS. Zero new dependencies — leverages existing brand assets and static-serving pipeline. | ||
|
|
||
| --- | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### PWA File Flow | ||
|
|
||
| ``` | ||
| ┌─────────────────────────────────────────┐ | ||
| │ frontend/public/ │ | ||
| │ │ | ||
| │ manifest.webmanifest ─── Vite ───► dist/manifest.webmanifest | ||
| │ sw.js ─── Vite ───► dist/sw.js | ||
| │ │ | ||
| │ Containerfile │ | ||
| │ mv frontend/dist public ───────► /app/public/ | ||
| │ │ | ||
| │ Express (server.js:2652) │ | ||
| │ express.static(staticPath) ─────► /manifest.webmanifest | ||
| │ /sw.js | ||
| └─────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| ### Service Worker Strategy | ||
|
|
||
| Network-first for all GET requests. Cache only the app shell (`/`). Falls back to cached shell on navigation failure. Does not intercept non-GET requests. | ||
|
|
||
| --- | ||
|
|
||
| ## Technology Choices | ||
|
|
||
| | Component | Technology | Rationale | | ||
| |-----------|------------|-----------| | ||
| | Manifest | Static `.webmanifest` file | No build plugin needed; Vite copies public/ verbatim | | ||
| | Service Worker | Hand-written vanilla JS | Avoids Workbox dependency; installability requires minimal SW | | ||
| | Icons | Existing brand PNGs | 192x192 and 512x512 already in `frontend/public/brand/` | | ||
|
|
||
| --- | ||
|
|
||
| ## Implementation Steps | ||
|
|
||
| ### Phase 1: New Files | ||
|
|
||
| - [ ] Create `frontend/public/manifest.webmanifest` | ||
| - [ ] Create `frontend/public/sw.js` | ||
|
|
||
| ### Phase 2: Modify Existing Files | ||
|
|
||
| - [ ] Add PWA meta tags to `frontend/index.html` | ||
| - [ ] Add SW registration to `frontend/src/main.jsx` | ||
|
|
||
| ### Phase 3: Verify | ||
|
|
||
| - [ ] `./run.sh reload-app` and verify manifest/sw.js served correctly | ||
| - [ ] Check Chrome DevTools Application tab | ||
| - [ ] `./run.sh build` passes | ||
| - [ ] User verifies install prompt on mobile | ||
|
|
||
| --- | ||
|
|
||
| ## File Changes | ||
|
|
||
| ### New Files | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `frontend/public/manifest.webmanifest` | Web app identity, icons, display mode | | ||
| | `frontend/public/sw.js` | Minimal service worker for installability | | ||
|
|
||
| ### Modified Files | ||
|
|
||
| | File | Changes | | ||
| |------|---------| | ||
| | `frontend/index.html` | Add manifest link, theme-color, Apple PWA meta tags | | ||
| | `frontend/src/main.jsx` | Add service worker registration | | ||
|
|
||
| --- | ||
|
|
||
| ## Testing Strategy | ||
|
|
||
| ### Manual Testing | ||
|
|
||
| 1. Visit `/manifest.webmanifest` — returns valid JSON with correct content type | ||
| 2. Visit `/sw.js` — returns JavaScript | ||
| 3. Chrome DevTools → Application → Manifest: shows name, icons, colors | ||
| 4. Chrome DevTools → Application → Service Workers: shows `sw.js` active | ||
| 5. Lighthouse audit → Installable: passes | ||
| 6. Android Chrome: "Add to Home Screen" prompt appears | ||
| 7. iOS Safari: Share → "Add to Home Screen" works | ||
|
|
||
| --- | ||
|
|
||
| ## Rollback Plan | ||
|
|
||
| If issues are discovered: | ||
| 1. Remove `<link rel="manifest">` from index.html to disable installability | ||
| 2. Remove SW registration from main.jsx | ||
| 3. SW will go dormant (no fetch handler registered on next load) | ||
|
|
||
| --- | ||
|
|
||
| ## Risks and Mitigations | ||
|
|
||
| | Risk | Impact | Mitigation | | ||
| |------|--------|------------| | ||
| | Stale cached HTML after deploy | Low | Network-first strategy; cache only used on network failure | | ||
| | OG tag middleware conflict | None | OG middlewares only target og:/twitter:/title tags, not manifest/theme-color | | ||
| | iOS Safari limitations | Low | apple-mobile-web-app-capable meta tag handles iOS; no push notifications needed | | ||
|
|
||
| --- | ||
|
|
||
| ## Changelog | ||
|
|
||
| | Date | Changes | | ||
| |------|---------| | ||
| | 2026-05-27 | Initial plan | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Specification: Progressive Web App (PWA) Support | ||
|
|
||
| > **Spec ID:** 028-pwa | ||
| > **Status:** In Progress | ||
| > **Version:** 0.1.0 | ||
| > **Author:** Scott McCarty | ||
| > **Date:** 2026-05-27 | ||
|
|
||
| ## Overview | ||
|
|
||
| Add Progressive Web App support so users can install Roots of The Valley on their phone (Android or iOS) directly from the browser. The installed app launches in standalone mode with its own icon, splash screen, and no browser chrome — looking and feeling like a native app while remaining the same website under the hood. | ||
|
|
||
| --- | ||
|
|
||
| ## User Stories | ||
|
|
||
| ### Installation | ||
|
|
||
| **US-028-1: Install on Android** | ||
| > As a mobile user on Android, I want to install ROTV from my browser so that I can launch it from my home screen like a native app. | ||
|
|
||
| Acceptance Criteria: | ||
| - [ ] Chrome on Android shows "Add to Home Screen" or install prompt when visiting rootsofthevalley.org | ||
| - [ ] Installed app appears on home screen with ROTV icon and "ROTV" label | ||
| - [ ] App launches in standalone mode (no URL bar or browser chrome) | ||
| - [ ] App uses forest green (#2d5016) for the Android status bar | ||
|
|
||
| **US-028-2: Install on iOS** | ||
| > As a mobile user on iPhone, I want to add ROTV to my home screen so that I can access it like an app. | ||
|
|
||
| Acceptance Criteria: | ||
| - [ ] Safari on iOS shows "Add to Home Screen" option via Share menu | ||
| - [ ] Installed app appears on home screen with ROTV icon and "ROTV" label | ||
| - [ ] App launches in standalone mode (no Safari chrome) | ||
|
|
||
| ### Manifest & Identity | ||
|
|
||
| **US-028-3: App Identity** | ||
| > As a user installing the app, I want to see correct branding (name, icon, colors) so that the app looks professional and trustworthy. | ||
|
|
||
| Acceptance Criteria: | ||
| - [ ] Web app manifest declares name "Roots of The Valley" and short_name "ROTV" | ||
| - [ ] Manifest references 192x192 and 512x512 PNG icons from existing brand assets | ||
| - [ ] Theme color (#2d5016) and background color (#f5f5f5) match the website design | ||
| - [ ] Lighthouse PWA audit "Installable" section passes | ||
|
|
||
| --- | ||
|
|
||
| ## Data Model | ||
|
|
||
| No database changes required. | ||
|
|
||
| --- | ||
|
|
||
| ## API Endpoints | ||
|
|
||
| No API changes required. | ||
|
|
||
| --- | ||
|
|
||
| ## UI/UX Requirements | ||
|
|
||
| ### Meta Tags | ||
|
|
||
| The following meta tags are added to `index.html`: | ||
| - `<link rel="manifest">` pointing to the web app manifest | ||
| - `<meta name="theme-color">` for browser/OS UI theming | ||
| - Apple-specific meta tags for iOS home screen support | ||
|
|
||
| ### No New Components | ||
|
|
||
| No React components are added. PWA support is purely infrastructure (manifest, service worker, meta tags). | ||
|
|
||
| --- | ||
|
|
||
| ## Non-Functional Requirements | ||
|
|
||
| **NFR-028-1: No New Dependencies** | ||
| - No npm packages added (no vite-plugin-pwa, no Workbox) | ||
| - Manual, lightweight implementation | ||
|
|
||
| **NFR-028-2: No Offline Caching of External Resources** | ||
| - Service worker does NOT cache map tiles, API responses, or CDN resources | ||
| - App remains online-dependent (map tiles require network) | ||
| - Only the app shell (index.html) is cached for installability | ||
|
|
||
| **NFR-028-3: No Backend Changes** | ||
| - Existing Express static-serving pipeline serves new files without modification | ||
|
|
||
| --- | ||
|
|
||
| ## Dependencies | ||
|
|
||
| - Depends on: Existing brand icons at 192x192 and 512x512 (already present) | ||
| - Depends on: HTTPS in production (already configured) | ||
| - Blocks: None | ||
|
|
||
| --- | ||
|
|
||
| ## Open Questions | ||
|
|
||
| None — all design decisions resolved during planning. | ||
|
|
||
| --- | ||
|
|
||
| ## Changelog | ||
|
|
||
| | Version | Date | Changes | | ||
| |---------|------|---------| | ||
| | 0.1.0 | 2026-05-27 | Initial draft | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "name": "Roots of The Valley", | ||
| "short_name": "ROTV", | ||
| "description": "Explore the rich history and natural beauty of Cuyahoga Valley National Park.", | ||
| "start_url": "/", | ||
| "display": "standalone", | ||
| "theme_color": "#2d5016", | ||
| "background_color": "#f5f5f5", | ||
| "icons": [ | ||
| { | ||
| "src": "/brand/rotv-favicon-192x192.png", | ||
| "sizes": "192x192", | ||
| "type": "image/png" | ||
| }, | ||
| { | ||
| "src": "/brand/rotv-favicon-512x512.png", | ||
| "sizes": "512x512", | ||
| "type": "image/png" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| var CACHE_NAME = 'rotv-shell-v1'; | ||
| var SHELL_URLS = ['/']; | ||
|
|
||
| self.addEventListener('install', function (event) { | ||
| event.waitUntil( | ||
| caches.open(CACHE_NAME).then(function (cache) { | ||
| return cache.addAll(SHELL_URLS); | ||
| }) | ||
| ); | ||
| self.skipWaiting(); | ||
| }); | ||
|
|
||
| self.addEventListener('activate', function (event) { | ||
| event.waitUntil( | ||
| caches.keys().then(function (keys) { | ||
| return Promise.all( | ||
| keys.filter(function (k) { return k !== CACHE_NAME; }) | ||
| .map(function (k) { return caches.delete(k); }) | ||
| ); | ||
| }) | ||
| ); | ||
| self.clients.claim(); | ||
| }); | ||
|
|
||
| self.addEventListener('fetch', function (event) { | ||
| if (event.request.method !== 'GET') return; | ||
|
|
||
| event.respondWith( | ||
| fetch(event.request).catch(function () { | ||
| if (event.request.mode === 'navigate') { | ||
| return caches.match('/'); | ||
| } | ||
| return caches.match(event.request); | ||
| }) | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,3 +11,11 @@ ReactDOM.createRoot(document.getElementById('root')).render( | |||||||||||||||||||||||||||||||||||||||||
| </BrowserRouter> | ||||||||||||||||||||||||||||||||||||||||||
| </React.StrictMode> | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if ('serviceWorker' in navigator) { | ||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('load', () => { | ||||||||||||||||||||||||||||||||||||||||||
| navigator.serviceWorker.register('/sw.js').catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||
| console.warn('SW registration failed:', err); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the script is loaded asynchronously or deferred (which is common for Vite module scripts), the browser's To ensure reliable registration, check
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning the cached
/(index.html) when offline or on network failure will result in a White Screen of Death (blank page) for the user.Why this happens:
/(index.html) during installation. It does not cache the compiled JS and CSS assets (which are hashed by Vite, e.g.,index-XXXX.js).index.html.Solution:
Instead of returning the cached
/which depends on non-cached external assets, return a self-contained, branded offline HTML fallback response directly from the service worker. This satisfies the PWA installability requirement (serving a 200 OK when offline) while providing a clean, user-friendly offline message instead of a broken blank screen.