| 1 | # Rastrillo PWA |
| 2 | |
| 3 | Add an installable app manifest, a public offline page and controlled worker |
| 4 | updates to a web app. This optional module has no runtime dependencies. |
| 5 | Web Push composes through [aviso](https://amadan.net/rastrillo/aviso). |
| 6 | |
| 7 | The kit does not store pages, API responses, messages, keys or pending writes. |
| 8 | Offline navigation shows a public fallback. Offline reading and editing need |
| 9 | an application data model and synchronisation contract; see |
| 10 | [docs/offline.md](docs/offline.md). |
| 11 | |
| 12 | ## Try it |
| 13 | |
| 14 | From this checkout: |
| 15 | |
| 16 | ```sh |
| 17 | cd examples/basic |
| 18 | go run . |
| 19 | ``` |
| 20 | |
| 21 | Open `http://127.0.0.1:8080`, reload once after the worker has installed, |
| 22 | then disconnect and reload. The worker returns an offline page with a retry |
| 23 | link. Connect again and retry to return to the app. The sample serves valid |
| 24 | PNG icons as placeholders; replace them with your app's icons. |
| 25 | |
| 26 | The sample imports aviso's worker helper and wires notification handlers. |
| 27 | It does not enrol subscriptions or send push messages. Follow aviso's own |
| 28 | skill to provision its server and browser enrolment. |
| 29 | |
| 30 | ## Add it to an app |
| 31 | |
| 32 | Read [SKILL.md](SKILL.md). Install a reviewed version of |
| 33 | `amadan.net/rastrillo/pwa`. Create `pwa.Manifest` with a permanent ID, |
| 34 | name, start URL, scope ending in `/`, and your icons. Call `.Handler()` at |
| 35 | boot and handle its error. Serve the manifest and worker assets without |
| 36 | authentication redirects. Mount the returned handler at |
| 37 | `/manifest.webmanifest`. Mount assets with: |
| 38 | |
| 39 | ```go |
| 40 | mux.Handle("/pwa/", http.StripPrefix("/pwa", pwa.Assets())) |
| 41 | ``` |
| 42 | |
| 43 | Add the manifest link, theme colour and Apple touch icon to the page head: |
| 44 | |
| 45 | ```html |
| 46 | <link rel="manifest" href="/manifest.webmanifest"> |
| 47 | <meta name="theme-color" content="#234d45"> |
| 48 | <link rel="apple-touch-icon" href="/static/icon-180.png"> |
| 49 | ``` |
| 50 | |
| 51 | Serve your own `/sw.js` with `Content-Type: text/javascript` and |
| 52 | `Cache-Control: no-cache`: |
| 53 | |
| 54 | ```js |
| 55 | importScripts("/pwa/worker.js"); |
| 56 | RastrilloPWA.install(); |
| 57 | ``` |
| 58 | |
| 59 | Then register it from your page's JavaScript: |
| 60 | |
| 61 | ```js |
| 62 | import { register } from "/pwa/client.mjs"; |
| 63 | await register({onUpdate: () => { updateNotice.hidden = false; }}); |
| 64 | ``` |
| 65 | |
| 66 | Define `updateNotice` in the app. Suggested text: “An update is ready. Save |
| 67 | your work in all tabs, then close and reopen the app.” The helper neither |
| 68 | reloads pages nor automatically activates a waiting worker. Browser support |
| 69 | is detected; registration resolves to `null` without service workers. |
| 70 | |
| 71 | `activateUpdate(registration)` explicitly asks a waiting worker to activate |
| 72 | and returns whether there was one. Activation affects all tabs in its scope. |
| 73 | Use it only when the app has resolved unsaved work across those tabs; handle |
| 74 | `controllerchange` in the app if a reload is appropriate. Closing all tabs |
| 75 | allows normal browser activation without this helper. |
| 76 | |
| 77 | ## Worker contract |
| 78 | |
| 79 | `RastrilloPWA.install({offlineHTML})` attaches navigation and update-message |
| 80 | handlers once. `offlineHTML` is optional, trusted build-time public HTML. |
| 81 | The default is a self-contained English page. Provide an app-owned public |
| 82 | translation if needed; neither account details nor keys belong in it. |
| 83 | |
| 84 | Only same-origin, in-scope GET navigations are intercepted. A network |
| 85 | failure returns the fallback with status 503 and `Cache-Control: no-store`. |
| 86 | HTTP errors remain unchanged. API calls and mutations are untouched. The |
| 87 | fallback permits inline styles but no scripts, forms or external resources. |
| 88 | |
| 89 | The worker and imported scripts are persisted by the browser's worker |
| 90 | installation; the kit never writes Cache Storage or IndexedDB. With no |
| 91 | `clients.claim`, the first page stays uncontrolled until its next navigation. |
| 92 | Removing the worker registration removes the fallback capability. |
| 93 | |
| 94 | Use one worker registration per app scope. To add push, import aviso's |
| 95 | helper into this same `sw.js`, attach its push/click/subscription-change |
| 96 | handlers, and pass the same registration to its browser module. Read the |
| 97 | version-pinned aviso skill for its authentication and key-rotation contracts. |
| 98 | Keep notification payloads and encryption in the app. |
| 99 | |
| 100 | Production needs HTTPS. Installation UI differs by browser; on iOS guide |
| 101 | the person to add the app to the Home Screen and sign in inside that copy |
| 102 | before enabling push. Registration alone does not prompt installation or |
| 103 | grant notification permission. |
| 104 | |
| 105 | ## Validation |
| 106 | |
| 107 | `make ci` runs Go and JavaScript tests and a real Chromium/WebKit browser |
| 108 | drive. Install the matching Playwright browsers first with |
| 109 | `npx playwright install chromium webkit` after `npm ci`. Missing browsers |
| 110 | fail the gate. The nested example is built and tested by the gate too. |
| 111 | |
| 112 | The browser drive covers navigation failures, preservation of HTTP errors, |
| 113 | API failure behaviour, empty caches and updates across two edited tabs. |
| 114 | Chromium uses its offline switch; WebKit uses a dropped network connection |
| 115 | because its automation switch can abort before calling the worker. Device |
| 116 | installation and real iOS push delivery remain manual checks; this gate |
| 117 | does not claim them. |
| 118 | |
| 119 | MPL-2.0; see [LICENSE](LICENSE). |
| 120 | |