201 lines
5.5 KiB
Markdown
201 lines
5.5 KiB
Markdown
# PWA Implementation Notes
|
|
|
|
## Current state
|
|
|
|
The groundwork is already in place:
|
|
- `public/site.webmanifest` exists and is linked in `layout.ejs`
|
|
- All icon sizes are present (`favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`,
|
|
`apple-touch-icon.png`, `android-chrome-192x192.png`, `android-chrome-512x512.png`)
|
|
- The app is served from a single origin
|
|
|
|
---
|
|
|
|
## Step 1 — Fix the web manifest
|
|
|
|
`public/site.webmanifest` currently has empty `name`/`short_name` and wrong colors.
|
|
Update it to:
|
|
|
|
```json
|
|
{
|
|
"name": "Bri-Tunes",
|
|
"short_name": "Bri-Tunes",
|
|
"description": "Retrofuture music player",
|
|
"start_url": "/",
|
|
"display": "standalone",
|
|
"background_color": "#05060d",
|
|
"theme_color": "#ff2bd6",
|
|
"icons": [
|
|
{ "src": "/static/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
|
{ "src": "/static/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
|
]
|
|
}
|
|
```
|
|
|
|
Note: icon `src` paths need the `/static/` prefix since Express serves `public/` at that mount point.
|
|
The manifest itself is also served from `/static/site.webmanifest`.
|
|
|
|
Also add `theme-color` meta to `src/views/layout.ejs` so the browser chrome matches on mobile:
|
|
|
|
```html
|
|
<meta name="theme-color" content="#ff2bd6">
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2 — Write the service worker
|
|
|
|
Create `public/sw.js`. Recommended caching strategy for a server-rendered music app:
|
|
|
|
### Static shell — cache-first
|
|
Cache these on install so the app shell loads instantly on repeat visits:
|
|
|
|
```
|
|
/static/css/app.css
|
|
/static/js/player.js
|
|
/static/js/bg-grid.js
|
|
/static/js/player-viz.js
|
|
/static/js/boot-sequence.js
|
|
/static/vendor/fonts/Orbitron.woff2
|
|
/static/vendor/fonts/Inter.woff2
|
|
/static/vendor/three/three.module.min.js
|
|
/static/favicon.ico
|
|
/static/android-chrome-192x192.png
|
|
```
|
|
|
|
### HTML pages — network-first with cache fallback
|
|
Fetch from network; if offline serve the cached version. Cache `/` on install as the
|
|
offline fallback page.
|
|
|
|
### Audio streams (`/stream/*`) — do not cache
|
|
Audio files are large and frequently requested with HTTP Range headers.
|
|
The service worker should `return fetch(event.request)` for these and never
|
|
store them.
|
|
|
|
### Skeleton implementation
|
|
|
|
```js
|
|
const CACHE = 'bri-tunes-v1';
|
|
const STATIC = [
|
|
'/',
|
|
'/static/css/app.css',
|
|
'/static/js/player.js',
|
|
'/static/js/bg-grid.js',
|
|
'/static/js/player-viz.js',
|
|
'/static/js/boot-sequence.js',
|
|
'/static/vendor/fonts/Orbitron.woff2',
|
|
'/static/vendor/fonts/Inter.woff2',
|
|
'/static/vendor/three/three.module.min.js',
|
|
];
|
|
|
|
self.addEventListener('install', (e) => {
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(STATIC)));
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
|
)
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
self.addEventListener('fetch', (e) => {
|
|
const { request } = e;
|
|
const url = new URL(request.url);
|
|
|
|
// Never intercept audio streams or cross-origin requests.
|
|
if (url.pathname.startsWith('/stream/') || url.origin !== self.location.origin) return;
|
|
|
|
// Static assets: cache-first.
|
|
if (url.pathname.startsWith('/static/')) {
|
|
e.respondWith(
|
|
caches.match(request).then((cached) => cached || fetch(request))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// HTML pages: network-first, fall back to cache.
|
|
e.respondWith(
|
|
fetch(request)
|
|
.then((res) => {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((c) => c.put(request, clone));
|
|
return res;
|
|
})
|
|
.catch(() => caches.match(request))
|
|
);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Step 3 — Register the service worker
|
|
|
|
Add to `src/views/layout.ejs` just before `</body>`:
|
|
|
|
```html
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/static/sw.js');
|
|
}
|
|
</script>
|
|
```
|
|
|
|
The service worker file lives at `public/sw.js` and is served at `/static/sw.js`.
|
|
|
|
**Important:** the service worker's scope defaults to its path (`/static/`), which
|
|
limits it to only intercepting requests under `/static/`. To control the full origin
|
|
it must be served from `/sw.js` (i.e., at the root, not under `/static/`).
|
|
|
|
This means `sw.js` needs a **dedicated route** in Express rather than being served
|
|
as a static file:
|
|
|
|
```js
|
|
// In src/app.js, before the static middleware:
|
|
app.get('/sw.js', (req, res) => {
|
|
res.set('Service-Worker-Allowed', '/');
|
|
res.sendFile(path.join(__dirname, '..', 'public', 'sw.js'));
|
|
});
|
|
```
|
|
|
|
And register it at the root in the template:
|
|
|
|
```html
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/sw.js');
|
|
}
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## Step 4 — Bump cache version on deploy
|
|
|
|
When CSS/JS changes, increment the cache name (`bri-tunes-v1` → `bri-tunes-v2`).
|
|
The `activate` handler automatically evicts the old cache.
|
|
|
|
---
|
|
|
|
## Files to create / modify
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `public/site.webmanifest` | Fill in name, colors, fix icon paths |
|
|
| `src/views/layout.ejs` | Add `theme-color` meta, add SW registration script |
|
|
| `src/app.js` | Add `/sw.js` route with `Service-Worker-Allowed: /` header |
|
|
| `public/sw.js` | New — service worker (see skeleton above) |
|
|
|
|
---
|
|
|
|
## Out of scope (optional future work)
|
|
|
|
- **Offline song caching** — let users pin songs for offline playback. Requires
|
|
an explicit "Save offline" button, manual `cache.put()` calls, storage quota
|
|
management, and UI to show cached vs. uncached songs.
|
|
- **Background sync** — retry failed actions (e.g., playlist edits) when
|
|
connectivity returns.
|
|
- **Push notifications** — notify users when new songs are added.
|