PWA — Configuration and deployment
INFO
Usage here, contract elsewhere. This page explains how to use the feature. The contract — scope, configuration, exposed API, boundaries — lives in pwa.md. Where the two disagree, the specification wins.
GeoLeaf supports installation as a progressive web app (PWA) on Android and iOS, with no application-specific development. This page documents the configuration, the deployment prerequisites, and the per-platform behaviour.
Deployment prerequisites
| Requirement | Needed for |
|---|---|
| HTTPS | Lighthouse PWA, Service Worker, beforeinstallprompt |
| Active Service Worker | Offline support, Lighthouse PWA |
Valid manifest.json | Installability, Lighthouse PWA |
192 and 512 px icons (including maskable) | Lighthouse PWA score of 100 |
<link rel="manifest"> in the HTML | Browser detection |
theme-color meta tag | Browser chrome colour |
The Service Worker (
sw-core.js) and the manifest (manifest.json) are included automatically in the variants built bybuild-deploy.cjs.
Generating the icons
The PWA icons must be generated from the source logo before the first deployment:
node scripts/generate-pwa-icons.cjsThis produces 4 files in apps/geoleaf-app/src/assets/icons/:
| File | Size | Purpose |
|---|---|---|
icon-192.png | 192×192 | Standard — Android, Windows |
icon-512.png | 512×512 | Standard — splash screen |
icon-192-maskable.png | 192×192 | Maskable — Android adaptive icons |
icon-512-maskable.png | 512×512 | Maskable — maskable splash screen |
Prerequisite:
sharpinstalled inpackages/core(npm install --prefix packages/core).
Configuring the PWA branding
The manifest is generated at build time by build-deploy.cjs, by merging:
- The source template
apps/geoleaf-app/manifest.json - The
pwasection ofprofiles/geoleaf.config.json
The pwa.* fields of the configuration override the template values:
// profiles/geoleaf.config.json
{
"pwa": {
"name": "Mon Application",
"short_name": "MonApp",
"description": "Ma description",
"theme_color": "#2d6a4f",
"background_color": "#ffffff",
"installPrompt": {
"enabled": false
}
}
}Enabling the install prompt
The prompt is disabled by default (enabled: false). To enable it:
// profiles/geoleaf.config.json
{
"pwa": {
"installPrompt": {
"enabled": true
}
}
}GeoLeaf.PWA.init() is called automatically once the configuration has loaded (in app/boot.ts).
Per-platform behaviour
| Platform | Behaviour |
|---|---|
| Android / Chrome / Edge | Captures beforeinstallprompt → shows a custom banner with an install button |
| iOS Safari | beforeinstallprompt does not exist → shows a manual banner explaining how to add the app to the home screen through the Share menu |
| Other browsers | No beforeinstallprompt → no banner |
Android banner
- Appears only once the browser considers the app installable (HTTPS + Service Worker + valid manifest)
- Dismissal is persisted in
localStorage['gl_pwa_install_dismissed'] - Hides itself after installation (
appinstalledevent)
iOS banner
- Detects
/(iPhone|iPad|iPod)/iinnavigator.userAgent - Not shown when the app already runs in standalone mode (
navigator.standalone === true) - Appears 1.5 seconds after load, so it does not hide the initial content
- Dismissal is persisted in
localStorage['gl_pwa_ios_dismissed']
Programmatic use (ESM)
import { PWA } from "@geoleaf/core";
// Manual initialisation (normally called by boot.ts)
PWA.init({
installPrompt: { enabled: true },
});Through the global namespace (CDN/ESM):
GeoLeaf.PWA.init({ installPrompt: { enabled: true } });Testing locally
The Service Worker and the manifest are not active when the sources are served directly: they only exist in the built variants. A deployment must therefore be regenerated, in four steps — the first one is not optional:
npx turbo run build && npm run build:deploy && node scripts/build-deploy-coverage.cjs && npm run build:deploy:localThen point a browser at the vhost that serves deploy/ (a server is already running; do not start a second one). The Service Worker requires HTTPS, or localhost for testing.
WARNING
Running npm run build:deploy on its own rebuilds only part of what it copies, so it produces a stale deployment while still exiting 0. Any test run against that output measures the previous bundle.
Lighthouse validation
To reach a PWA score of 90 or more (ideally 100):
npx lighthouse https://your-domain.com --preset=pwa --output=html --output-path=./lighthouse-report.htmlChecklist:
- [ ] HTTPS active on the server
- [ ]
manifest.jsonpresent at the root, withname, 192+512icons,start_urlanddisplayfilled in - [ ] Icons with
purpose: maskable(produced bygenerate-pwa-icons.cjs) - [ ]
<link rel="manifest">in the HTML - [ ]
theme-colormeta tag present - [ ] Service Worker registered,
fetchevent handler active - [ ]
start_urlanswers HTTP 200 (including offline)
File layout
apps/geoleaf-app/ ← the deployable APPLICATION, source of the deploy/ variants
├── manifest.json ← Source template (merged by build-deploy.cjs)
├── init.js ← SW registration (navigator.serviceWorker.register)
└── src/assets/icons/
├── icon-192.png ← Generated by generate-pwa-icons.cjs
├── icon-192-maskable.png
├── icon-512.png
└── icon-512-maskable.png
packages/core/src/
├── api/geoleaf.pwa.ts ← Public facade (GeoLeaf.PWA)
├── kernel/storage/sw-core.js ← Lite Service Worker (static cache + profiles)
└── capabilities/pwa/
├── pwa-capability.ts ← Capability declaration (gate, configSchema)
├── pwa-manager.ts ← Orchestrator + PWAConfig interface
├── install-prompt.ts ← Android banner
├── ios-banner.ts ← iOS instructions banner
├── platform.ts ← Platform detection
├── lifecycle.ts · install.ts · public-api.ts
scripts/
└── generate-pwa-icons.cjs ← Icon generator (sharp)PWAConfig interface reference
See {@link PWAConfig} for the complete field documentation.
