Skip to content

feat: installable PWA, offline pages, and SEO metadata - #147

Open
AnasIsmai1 wants to merge 15 commits into
OpenMouse-Project:devfrom
AnasIsmai1:feat/pwa-offline-seo
Open

feat: installable PWA, offline pages, and SEO metadata#147
AnasIsmai1 wants to merge 15 commits into
OpenMouse-Project:devfrom
AnasIsmai1:feat/pwa-offline-seo

Conversation

@AnasIsmai1

@AnasIsmai1 AnasIsmai1 commented Aug 30, 2026

Copy link
Copy Markdown

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)

  • No hardware involved, so this was verified in Chromium instead: the worker activates, every precached URL resolves, the manifest icons load, the API stays uncached, all five pages render with the network off, and the offline notice follows the connection state.
  • A test asserts that every page in the repository is in the precache list, so adding a page cannot quietly break offline support the way the contribution guide would have. I checked that it fails when a page is removed.
  • The supported devices page is still rendered in the browser, so a crawler that does not run JavaScript sees an empty shell. Rendering it at build time is worth doing but belongs in its own change.
  • The control app keeps its noindex directive deliberately. Say the word if you would rather it be indexed; it is a one-line change.
  • Canonical URLs point at the production domain, so a preview deploy will not compete in the index.
  • Cost is under 600 bytes each of CSS and JavaScript, with both budgets unchanged.

Checklist

  • Code follows project coding standards
  • Best practices are followed
  • The issue is fully resolved
  • Changes are tested locally — npm run check green (90/90) and bundle-size check passes
  • Unit test added — precache coverage, verified failing when a page is dropped
  • E2E test added — none; verified by hand in Chromium
  • Demo video uploaded OR screenshots attached (if applicable) — happy to add offline and install captures if useful
  • No breaking changes OR breaking changes are clearly mentioned — the old hand-written worker is deleted and replaced by the generated one
  • No new dependencies OR dependencies are documented — none added
  • Cloud / deployment changes required (clearly mentioned if yes) — none
  • Database migration required (clearly mentioned if yes) — none
  • No sensitive data (keys, secrets) included
  • Logs and debug code removed
@jazzstack
jazzstack requested a review from snekxs August 30, 2026 13:23
@AnasIsmai1 AnasIsmai1 changed the title feat: installable PWA, offline pages, and SEO Aug 30, 2026
@AnasIsmai1 AnasIsmai1 changed the title pwa: link the manifest, precache every page, and add page metadata Aug 30, 2026
@jazzstack

jazzstack commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

@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 👍

@AnasIsmai1
AnasIsmai1 force-pushed the feat/pwa-offline-seo branch from 1d9008c to 49d570f Compare August 30, 2026 17:03
@AnasIsmai1

Copy link
Copy Markdown
Author

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: /api/, /control-app, /protected-assets/, /control(.html). I also added a guard that refuses to store any response carrying no-store, so a future licensed route is covered even if nobody remembers the regex. Tests assert both that those paths bypass and that ordinary pages still cache.

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 robots.txt is byte-identical to what I added, but sitemap.xml isn't: main lists / and /demo.html, this lists the dev pages. That needs a decision at merge rather than a resolve.

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.
@AnasIsmai1
AnasIsmai1 force-pushed the feat/pwa-offline-seo branch from 49d570f to bdddf39 Compare August 30, 2026 17:14
@jazzstack

Copy link
Copy Markdown
Contributor

@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.
@AnasIsmai1

Copy link
Copy Markdown
Author

Fixed, staleWhileRevalidate goes through storable() now. Folded opaque into it rather than swapping the check outright, since opaque responses report ok === false and gstatic fonts would have stopped caching.

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 github: shorthand, the same regression e4afaec fixed. Might be worth landing both on dev directly instead of waiting on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants