| 1 | # Making a rastrillo app installable |
| 2 | |
| 3 | Aviso does not own any of this, on purpose: a manifest is the app's |
| 4 | identity, and rastrillo's scaffold is not changed by the addon. |
| 5 | Android and desktop browsers deliver push to an ordinary website. iOS |
| 6 | and iPadOS (from 16.4) deliver it only to an app added to the Home |
| 7 | Screen, and only from that installed copy — so an app that wants push |
| 8 | on iPhones wants this recipe too. It is four things. |
| 9 | |
| 10 | ## 1. A manifest |
| 11 | |
| 12 | Serve `manifest.webmanifest` (content type |
| 13 | `application/manifest+json`) with, at least: |
| 14 | |
| 15 | ```json |
| 16 | { |
| 17 | "id": "/", |
| 18 | "name": "Birthday Alarm", |
| 19 | "short_name": "Birthdays", |
| 20 | "start_url": "/", |
| 21 | "scope": "/", |
| 22 | "display": "standalone", |
| 23 | "theme_color": "#5b6cff", |
| 24 | "background_color": "#f6f7fb", |
| 25 | "icons": [ |
| 26 | { "src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png" }, |
| 27 | { "src": "/static/icon-512.png", "sizes": "512x512", "type": "image/png" } |
| 28 | ] |
| 29 | } |
| 30 | ``` |
| 31 | |
| 32 | `id` is what keeps an installed app the same app across a renamed |
| 33 | `start_url`; set it once and never change it. A twenty-line handler |
| 34 | that writes this from the app's name is enough — Eleven's |
| 35 | `serveManifest` is the model — and a static file is enough too. |
| 36 | |
| 37 | ## 2. Three tags in the layout's head |
| 38 | |
| 39 | ```html |
| 40 | <link rel="manifest" href="/manifest.webmanifest"> |
| 41 | <meta name="theme-color" content="#5b6cff"> |
| 42 | <link rel="apple-touch-icon" href="/static/icon-180.png"> |
| 43 | ``` |
| 44 | |
| 45 | Safari reads the Apple touch icon, not the manifest's icons, for the |
| 46 | Home Screen tile; 180 px is the size it wants. |
| 47 | |
| 48 | ## 3. The worker at its scope |
| 49 | |
| 50 | Serve `sw.js` at the path whose scope it should control — `/sw.js` |
| 51 | for the whole app — with `Cache-Control: no-cache`, so a new worker is |
| 52 | noticed on the next load rather than after a cache expiry nobody |
| 53 | chose. Load the helper from inside it: |
| 54 | |
| 55 | ```js |
| 56 | importScripts("/static/aviso/aviso-sw.js"); |
| 57 | ``` |
| 58 | |
| 59 | The rest of the worker is in `SKILL.md`. |
| 60 | |
| 61 | ## 4. Coaching, keyed on `capabilities()` |
| 62 | |
| 63 | `capabilities()` from `push.mjs` reports `standalone`: whether this |
| 64 | page is running as an installed app. On iOS and iPadOS, when it is |
| 65 | not, show the person how to add the app to their Home Screen (Share → |
| 66 | Add to Home Screen) and to sign in inside the installed copy before |
| 67 | pressing the enable button — Safari's cookies do not travel into the |
| 68 | installed app. Do not gate Android or desktop on installation; they |
| 69 | do not need it. Show the enable button only when `capabilities().push` |
| 70 | is true, and only once `navigator.serviceWorker.ready` has resolved. |
| 71 | |
| 72 | That is the whole recipe. None of it is Web Push; all of it is what |
| 73 | Web Push needs on an iPhone, and a good idea everywhere else. |
| 74 | |