feat: installable PWA, offline pages, and SEO metadata - #147
Conversation
|
@AnasIsmai1 i went n actually checked main and it changes things a bit.... the licensed surface it exists on main main has control-app.html n protected-assets/control-[hash].js (thats the actual licensed bundle), with the middleware gate-keeping /control-app* n /protected-assets/* per request via a supabase session cookie — 404/redirect when revoked — and _routes.json routing those two through the functions. so it def exists, just not on dev which is what my first comment was based on. so on dev this pr is totally safe, but this code has to merge to main eventually and the new handler is cacheFirst — it caches any same-origin GET and serves it from cache without ever hitting the network. after one authorized visit the control-app shell + protected-assets bundle sit in the sw cache, n the middleware check (n its Cache-Control: private, no-store) just never run again. revoked license keeps working basically. the current main worker avoids exactly that with the /control-app/ + /protected-assets/ bypass regexes. keeping em is one line n does nothing on dev so id keep them so this doesnt turn into a license bypass when it lands on main. everything else is great the vary fix, the generateBundle rewrite, per-entry install, the /devices/ runtime caching, offline banner.... just want the bypass list kept 👍 |
1d9008c to
49d570f
Compare
|
You're right, thanks for going and checking main. I built the bypass list off dev, where those routes don't exist, so I cut them as dead entries. They're not dead, they're just not here yet. All four are back: Two other things from main while I was in there. The icons I'd generated already exist there and are the designed ones, so I dropped mine and took main's, which also kills a binary conflict at merge. And Also dropped the OG image from the precache list, since scrapers fetch it server side and the page never asks for it, and raised the JS budget to 710 kB. dev was already at 717.7 kB of a 718 kB ceiling before this branch, so the 577 B here tipped it over. |
The PWA pieces existed but were never wired to each other: the manifest was linked from no page and named four icons that did not exist, the good service-worker generator in build/ was never imported, and the hand-written public/sw.js precached only index.html. Three of the five pages had no description, canonical, or social card, and there was no robots.txt or sitemap.xml. - Generate the missing icons from logo.png and add an OG image - Wire build/pwa-vite-plugin.ts into vite.config.ts, drop public/sw.js - Precache all five pages, their hashed assets, and the icons - Match cached entries with ignoreVary: responses carry "Vary: Origin", which made every module-script lookup miss and broke offline entirely - Cache same-origin assets as they are requested, so device art under /devices/ survives offline despite never appearing in the markup - Install per entry rather than addAll, so one bad URL cannot leave the app with no offline copy at all - Register the worker from every page through src/register-sw.ts - Add an offline banner with a polite live region - Add robots.txt, sitemap.xml, canonicals, OG and Twitter cards, and JSON-LD; index.html stays noindex
The bypass list was cut down to /api/ because nothing else it named exists on dev. Those routes do exist on main: _routes.json sends /control-app* and /protected-assets/* through functions/_middleware.js, which validates a session cookie per request and answers no-store. Caching either one would let a revoked licence keep working, so the entries have to survive the merge even though they are inert here. - Restore /control-app, /protected-assets/, and /control(.html) - Refuse to store any response carrying Cache-Control: no-store, so a future licensed route is covered without a matching regex - Take main's designed icons instead of the generated ones, avoiding a binary conflict at merge, and restore the dark favicon links - Drop the social card from the precache list; scrapers fetch it server side and the page never requests it - Cover the licensed bypasses and ordinary caching in the tests
dev reached 717.7 kB of the 718 kB effective ceiling on its own, so the 577 B this branch adds across the four page entries tips it over. Raises the base budget to 710 kB, which is 728 kB with the preview allowance, and restores about 10 kB of headroom for the next change.
49d570f to
bdddf39
Compare
|
@AnasIsmai1 the bypass list and storable() guard look good.....one small thing staleWhileRevalidate still caches on response.ok || opaque instead of going through storable()...its harmless for fonts today but worth aligning for consistency so a future routing change doesn't accidentally cache a no-store response |
The 730 kB raise for the device artwork pass covers this branch too, so no number changes. The file's convention is that every contribution to the ceiling carries its reason, and the merge resolution dropped the one written for the offline entries.
staleWhileRevalidate stored on response.ok || opaque, the one path that skipped the no-store check. Harmless while it only serves font origins, but it would not stay harmless if routing changed. Opaque responses report ok === false and expose no headers, so the check folds into storable() rather than replacing it, and fonts stay cacheable.
Every entry in PRECACHE_PAGES carried a url that was a pure function of its filename, and two of the tests did nothing but assert that formula against the hand-copied value. Derive it and drop both. The emitted worker is byte-identical.
Device art now comes from the R2 bucket, so it is cross-origin and never reaches the handler. What still lands there is same-origin chunks the pages do not link, which the precache scan cannot see.
dev outgrew 730 kB on its own. The R2 artwork move and the three SteelSeries additions took it to 750.2 kB against a 748 kB effective ceiling without a matching raise, so dev's own CI is red on a0079fe and 2f43267. This branch contributes 571 B of the 750.8 kB measured here. 750 kB is 768 kB with the preview allowance, leaving about 17 kB.
dev has been red on its own CI since a0079fe. The R2 artwork move, seven SteelSeries additions and two protocol bumps took dev alone to 780.0 kB against a 748 kB effective ceiling, without a matching raise. This branch contributes 577 B of the 780.6 kB measured here. 800 kB is 818 kB with the preview allowance. The ~37 kB of headroom is sized against the ~60 kB dev grew in a day, not the usual ~10 kB, so the next protocol bump does not reopen this.
a0079fe rewrote the spec to the github: shorthand, which is the exact regression e4afaec fixed before. CI compares package.json against a literal approved list and fails on the mismatch, so dev has been red on this since that commit. Restores the value CI declares. package-lock.json is untouched and npm ci resolves identically, same as e4afaec did.
|
Fixed, Heads up that dev is red on its own CI since a0079fe, for two things this PR happens to fix: it measures 780.0 kB against a 748 kB effective ceiling (raised to 800 kB here), and a0079fe rewrote the protocol spec to the |
Why is this PR needed?
The app had most of the pieces for a progressive web app but none of them were connected. The manifest was linked from no page and named three icons that had never been added to the repository, so the browser never offered to install anything. A service worker generator already lived in the build folder, versioned cache and all, and nothing imported it. The worker that actually shipped was an older hand-written one that cached only the front page under a fixed cache name that never invalidated. Registration happened on the control app alone, so the other four pages had no offline story at all.
On the search side there was no robots file, no sitemap, and no canonical, social, or structured data anywhere. The supported devices page, the one most worth indexing, had no description at all.
How was it solved?
This wires the existing generator into the build and deletes the old worker. The missing icons are generated from the logo already in the repository using a tool that ships with macOS, so nothing new is added to the dependency list. Every page and its hashed assets are precached, the API routes still bypass the cache, and the stale cache left behind by the old worker is cleared on activation.
Four things in the generator had to be fixed before any of that worked. It ran too late in the build to find the pages it was meant to read. Its cache lookups ignored the fact that responses vary on the request origin, so every script and stylesheet missed the cache and offline was broken end to end while navigation still appeared to work, which is what made it hard to spot. The install step failed as a whole if a single file was missing, leaving no offline copy rather than a partial one. And the runtime handler only read from the cache without ever writing to it, so the device artwork that gets chosen at runtime never survived a disconnect.
Every page with a script entry now registers the worker through one shared helper. The contribution guide is static and needs none, since the worker already covers the whole site. There is also a small offline notice that stays empty while the connection is up, so a screen reader only hears it when something actually changes.
The search side gets a robots file, a sitemap, canonical URLs, social cards, and structured data across the public pages. Two pages pointed at icons that do not exist and one pointed at a path that could never resolve; both are corrected. The control app stays out of the index and gets a social card only.
Notes for reviewers (if any)
Checklist
npm run checkgreen (90/90) and bundle-size check passes