This document is written as a technical instruction set for an AI coding agent that has access to an existing HTML/JavaScript astrology web app hosted on static hosting (for example, a static storage website). The goal is to convert the site into a production‑grade Progressive Web App (PWA) “with all the bells and whistles” where the platform allows it. citeturn1search24turn1search29turn0search12
Key constraint: “Full compatibility with Android and iOS” does not mean “every native feature on both OSes.” It means:
- Implement PWA capabilities using standards (manifest, service worker, modern Web APIs). citeturn1search29turn2search0turn5search5turn1search21
- Use progressive enhancement: enable features when the runtime supports them; provide fallbacks when it doesn’t. citeturn0search13turn1search7turn6search2
- Be honest about sandbox boundaries: many PWA APIs depend on secure contexts (HTTPS) and service workers, and some are limited/experimental or differ across browser engines. citeturn10search2turn10search27turn2search4turn11view0
iOS reality check (important for “full compatibility”):
- On iOS/iPadOS, push notifications for web apps exist (Home Screen web apps) from iOS/iPadOS 16.4, but permission must be requested via direct user interaction (e.g., tapping “Enable notifications”). citeturn4view0
- By Safari 26 / iOS 26 / iPadOS 26, any website a user adds to Home Screen opens as a web app by default unless the user disables “Open as Web App” during the add flow; this reduces “installability” friction, but does not remove the need for a manifest/service worker if you want deeper PWA functionality. citeturn4view1turn1search2
Architectural implication: because you’re on static hosting, the only unavoidable “non‑static” component for a fully featured PWA is push + reminders, since Web Push is designed for an application server to send pushes at any time. Therefore you will need a backend component (serverless is fine) for subscription management and scheduled deliveries. citeturn2search1turn2search0turn13search2
A web app manifest is a JSON file used by browsers/OS to treat a site like an app, including install metadata (name, icons, start URL, display mode), plus advanced features like shortcuts, share target, protocol handlers, and more. citeturn0search12turn6search8turn6search4turn5search0turn3search0turn5search2
At minimum you should define:
start_url(launch URL) citeturn0search0scope(navigation boundary) citeturn0search4display(standalone app‑like mode) citeturn0search8turn1search1turn1search21icons(multiple sizes) citeturn0search12turn4view0
Also strongly consider id (stable unique identity for the install, useful for multi‑installs and some OS behaviours). citeturn6search0turn4view0turn6search16
Template manifest (agent should adapt names/paths to project):
{
"id": "/?app=xingstar",
"name": "XingStar Astrology",
"short_name": "XingStar",
"description": "Astrology reports, reminders, and personalised insights.",
"lang": "en-GB",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"background_color": "#0B1020",
"theme_color": "#0B1020",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"shortcuts": [
{
"name": "Today’s horoscope",
"short_name": "Today",
"description": "Open today’s horoscope",
"url": "/?route=today",
"icons": [{ "src": "/icons/shortcut-today.png", "sizes": "96x96", "type": "image/png" }]
},
{
"name": "Reminders",
"short_name": "Reminders",
"description": "Manage reminder times",
"url": "/?route=reminders",
"icons": [{ "src": "/icons/shortcut-reminders.png", "sizes": "96x96", "type": "image/png" }]
}
],
"share_target": {
"action": "/share-handler",
"method": "GET",
"enctype": "application/x-www-form-urlencoded",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}This uses manifest members that are defined and documented in modern PWA references, including start_url, scope, display, shortcuts, and share_target. citeturn0search0turn0search4turn0search8turn5search0turn3search0
On iPhone/iPad, installation is typically done via Safari’s Share sheet → Add to Home Screen, and (on newer iOS) the user can toggle “Open as Web App.” citeturn1search2turn4view1
Agent instruction: implement an iOS install helper UI (non‑intrusive banner or modal) that appears only when:
- The app is running in a browser tab (not standalone mode), and
- The device appears to be iOS/iPadOS Safari/WebKit (feature detection first; UA detection only as a last resort), and
- The user has visited at least N times or reached a “moment of value” (after generating first report).
This is aligned with best practices: when beforeinstallprompt isn’t supported, you can only show instructions for manual install, and you should only do it when in browser mode. citeturn0search13turn6search3turn6search2
On Chromium browsers, the beforeinstallprompt event can allow a customised install CTA that triggers the browser install prompt later via event.prompt(). However this event is explicitly described as non‑standard and not universally supported. citeturn0search5turn0search1turn0search13turn0search25
Agent instruction: implement a custom install button using safe feature detection and a fallback when unavailable.
Install prompt capture skeleton:
let deferredInstallPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent automatic prompt
e.preventDefault();
deferredInstallPrompt = e;
window.dispatchEvent(new CustomEvent('pwa:install-available'));
});
async function promptInstall() {
if (!deferredInstallPrompt) return { outcome: 'unavailable' };
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice;
deferredInstallPrompt = null;
return choice; // { outcome: 'accepted'|'dismissed', platform: ... }
}This matches the documented behaviour of beforeinstallprompt and prompt() being a developer‑triggered install prompt hook (where supported). citeturn0search5turn0search1turn0search9
To differentiate installed standalone mode vs browser tab:
- Use
display-modemedia query viamatchMedia. citeturn6search2turn6search3turn6search10
Mode detection snippet:
function getDisplayMode() {
if (window.matchMedia('(display-mode: standalone)').matches) return 'standalone';
if (window.matchMedia('(display-mode: fullscreen)').matches) return 'fullscreen';
if (window.matchMedia('(display-mode: minimal-ui)').matches) return 'minimal-ui';
return 'browser';
}This is consistent with display-mode being intended for testing whether a web app is displayed as a tab vs standalone/fullscreen. citeturn6search2turn6search3turn6search10
Service workers act like a programmable proxy between app, browser, and network; they enable offline experiences, intercept requests, and provide access to push and background sync capabilities. citeturn2search0turn2search4turn2search32
They are available only in secure contexts (HTTPS, with localhost treated specially for development). citeturn10search27turn10search2turn1search24
The service worker must be registered from the same origin and its scope is tied to its location unless configured (and may require special headers to increase scope). citeturn10search0turn10search12turn10search16
Registration snippet:
async function registerServiceWorker() {
if (!('serviceWorker' in navigator)) return null;
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
return reg;
}register() associates the service worker script URL with a scope used for navigation/request matching. citeturn10search0turn10search15
Workbox provides standard caching patterns (cache-first, network-first, stale-while-revalidate, etc.). citeturn2search2turn2search10turn2search7turn2search34
For a real app that also needs Web Push and other complex logic, Workbox recommends injectManifest (you write the SW, Workbox injects precache manifest). This is explicitly called out: generateSW is not for service workers that include additional Service Worker features like Web Push. citeturn2search3turn2search37
Astrology apps usually have:
- A mostly static shell (HTML/CSS/JS, icons, fonts)
- API-driven personalised report data (user-specific)
- “Evergreen” content (help pages, zodiac descriptions)
- Potentially large generated artefacts (PDF reports, charts/images)
So implement a split strategy:
- Precache the app shell + offline page
- Navigation requests (
mode: navigate) → network-first with offline fallback - API requests → network-first with short fallback to cache + stale warning
- Images & static media → cache-first with expiration limits
- User-specific reports → store in IndexedDB as the true offline store (not only Cache Storage)
This aligns with service workers being used to build offline-first experiences and with caching strategy choice being app-dependent. citeturn2search4turn2search24turn3search2
/* global workbox */
import { clientsClaim } from 'workbox-core';
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute, setCatchHandler } from 'workbox-routing';
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
self.skipWaiting();
clientsClaim();
cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);
// HTML navigations: network-first + offline fallback
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({ cacheName: 'html' })
);
// API: network-first with short timeout
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
networkTimeoutSeconds: 3,
plugins: [new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 10 })]
})
);
// Static images: cache-first + expiration
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 400, maxAgeSeconds: 60 * 60 * 24 * 30 })]
})
);
// “Content” pages: stale-while-revalidate
registerRoute(
({ url }) => url.pathname.startsWith('/content/'),
new StaleWhileRevalidate({ cacheName: 'content' })
);
// Offline fallback handling
setCatchHandler(async ({ event }) => {
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
return Response.error();
});This approach uses documented Workbox strategies (including stale-while-revalidate as a standard runtime caching pattern). citeturn2search2turn2search6turn2search3turn2search4
Offline support is not just caching; it requires storage management (Cache Storage + IndexedDB + StorageManager). citeturn8search10turn8search2turn8search1turn2search12
Implement:
- Cache Storage for assets/responses (fast, but can be evicted). citeturn2search12
- IndexedDB for user data and report content (structured, persistent offline store). citeturn8search1turn8search5
- Persistent storage request to reduce eviction risk where supported (
navigator.storage.persist()). citeturn8search0turn8search2turn8search23
Persistent storage request:
async function tryPersistStorage() {
if (!navigator.storage?.persist) return false;
return await navigator.storage.persist();
}Persistent buckets are designed not to be cleared automatically under storage pressure without user consent, while best-effort storage may be evicted. citeturn8search21turn8search23turn8search0
Safari/WebKit storage: quota/enforcement varies by version and context. MDN documents that Safari/WebKit quota rules differ for browser apps vs embedded web content, and that saved Home Screen web apps use the “browser app” origin quota (modern Safari versions). citeturn8search15
Historically, WebKit documented a fixed Cache API quota of 50 MiB per partition in a WebKit engineering post; treat this as historical context and still test on target iOS versions. citeturn8search7
Agent should implement:
- A “new version available” UX (banner/modal) when a new service worker is waiting, and a “Reload to update” action.
- Ensure the SW script and precache manifest aren’t cached incorrectly by the HTTP cache.
Service workers update behaviour, lifecycle control, and the fact they can be terminated/restarted is fundamental; state must be persisted in storage (IndexedDB/Cache) rather than memory. citeturn2search32turn2search4turn2search0
The Push API + Notifications API + service worker is the standards-based stack for push notifications. citeturn2search1turn2search13turn7search35turn2search0
The Push API is explicitly designed so an application server can send a push message at any time, even if the user agent or web app is inactive; delivery goes through a push service and wakes the service worker. citeturn13search2turn2search1turn2search5
iOS specifics (must implement exactly like this):
- Web Push support exists for Home Screen web apps; permission requests must be initiated by direct user interaction. citeturn4view0
- Web Push on iOS uses Apple Push Notification service (APNs), and WebKit advises allowing
*.push.apple.comin server endpoint allowlists/firewalls as needed; no Apple Developer Program membership is required. citeturn4view0 - Home Screen web apps on iOS 16.4 also support the Badging API, including updates while handling push events. citeturn4view0turn3search1
(These constraints are essential if your “reminders” are a core product feature.)
Push requires:
- Browser registers SW
- User grants notification permission (UI/gesture)
- Client subscribes to PushManager using VAPID/application server key
- Client sends subscription to backend
- Backend stores subscriptions and sends pushes via Web Push Protocol with VAPID authentication
- SW handles
pushevent and shows notifications (and optionally sets badge) citeturn2search1turn9search5turn9search2turn2search5turn7search36turn3search1turn4view0
async function ensurePushSubscription({ vapidPublicKey }) {
if (!('serviceWorker' in navigator)) throw new Error('no-sw');
if (!('PushManager' in window)) throw new Error('no-push');
if (!('Notification' in window)) throw new Error('no-notification');
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
const reg = await navigator.serviceWorker.ready;
// Convert base64url VAPID key to Uint8Array
const key = base64UrlToUint8Array(vapidPublicKey);
// userVisibleOnly is required in some browsers like Chrome/Edge.
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: key
});
return sub.toJSON();
}
function base64UrlToUint8Array(base64Url) {
const padding = '='.repeat((4 - (base64Url.length % 4)) % 4);
const base64 = (base64Url + padding).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(base64);
const out = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
return out;
}PushManager.subscribe()returns a subscription containing endpoint + keys. citeturn2search1turn9search0turn9search4userVisibleOnly: trueis required in some browsers like Chrome and Edge (otherwise subscribe is rejected). citeturn13search1turn13search12- VAPID is defined by an IETF RFC and is used to identify/authenticate the application server to push services. citeturn9search2turn9search6turn9search5turn9search12
self.addEventListener('push', (event) => {
let data = {};
try { data = event.data ? event.data.json() : {}; } catch {}
const title = data.title || 'XingStar';
const options = {
body: data.body || 'Your astrology update is ready.',
icon: '/icons/icon-192.png',
badge: '/icons/badge.png',
data: data.data || {},
tag: data.tag || 'xingstar-reminder',
renotify: false
};
event.waitUntil((async () => {
await self.registration.showNotification(title, options);
// Optional: update app badge if supported
if (self.navigator?.setAppBadge && typeof data.badgeCount === 'number') {
await self.navigator.setAppBadge(data.badgeCount);
}
})());
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url || '/?route=notifications';
event.waitUntil(clients.openWindow(url));
});- Push messages are delivered to the service worker
pushevent. citeturn2search5turn2search9turn2search1 - Notifications from SW are shown using
ServiceWorkerRegistration.showNotification(). citeturn7search36turn2search1 - Badging API uses
setAppBadge()/clearAppBadge()(supported in iOS Home Screen web apps from iOS 16.4 per WebKit). citeturn3search1turn4view0turn3search9
For backend implementation, the agent can use any language; for speed, Node.js using a well-known library is often easiest. web.dev provides detailed guidance on the Web Push protocol and VAPID/application server keys. citeturn9search1turn9search5turn9search9
Node.js pseudo‑implementation sketch:
import webpush from 'web-push';
webpush.setVapidDetails(
'mailto:support@yourdomain.example',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
// subscription is the JSON from client: { endpoint, keys: { p256dh, auth } }
async function sendPush(subscription, payloadObj) {
const payload = JSON.stringify(payloadObj);
await webpush.sendNotification(subscription, payload, {
TTL: 60 * 60 // 1 hour
});
}VAPID is formally specified by the IETF, and web.dev describes how application server keys/VAPID are used to authenticate to push services. citeturn9search2turn9search1turn9search5turn9search12
If reminders must work on both Android and iOS: implement reminders as server‑scheduled Web Push:
- User sets reminder time (and timezone) in app
- Backend stores schedule
- A scheduler/cron triggers pushes at the required time for each user
- Service worker shows notification
This is aligned with the Push API model: application server can send push at any time. citeturn13search2turn2search1
Optional Android-only enhancement: Notification Triggers API can schedule local notifications without network, but it is an experimental capability and not the iOS-compatible primary plan. citeturn7search0turn7search16
Background Sync lets the app defer tasks until connectivity is restored, implemented via service worker sync events; however MDN marks it as limited availability/experimental. citeturn11view0turn3search6turn3search14
Periodic Background Sync is also experimental and availability is limited; treat it as optional. citeturn3search3turn3search15turn3search11
Recommendation for production: implement a reliable offline queue using IndexedDB + “sync when online” in the foreground, and optionally register background sync when supported.
This section enumerates “bells and whistles” beyond the home screen icon; each item includes why it helps and how to implement, with the assumption that availability varies by platform.
Use shortcuts to deep-link into core actions: today’s horoscope, generate report, reminders, saved reports. Shortcuts are defined in the manifest and are meant to improve productivity and feel more native. citeturn5search0turn5search17turn5search13
- Web Share API: let users share a horoscope/report link, text, and (where supported) files through OS share UI. citeturn3search31
- Share Target: register your installed PWA as a target in the OS share sheet so other apps can send content into your PWA. citeturn3search0turn3search4turn3search8
For astrology, this can support:
- Sharing “today’s horoscope” as a link card.
- Inbound share: user shares an image or note into the app to attach to a journal entry (where file share targeting is supported). citeturn3search4turn3search8
Badges can represent unread reports/reminders and integrate with notification workflows. citeturn3search1turn4view0turn3search17
Launch handler allows controlling whether launches focus an existing app window or open a new one, via launch_handler in the manifest, with LaunchQueue available for custom handling. citeturn5search3turn5search9turn5search16turn5search12
Useful for astrology:
- If user taps multiple notifications, focus the same app instance and route internally.
Protocol handlers allow the PWA to register to handle URL schemes (e.g., web+astrology://...) via manifest protocol_handlers. citeturn5search2turn5search23turn5search15
Use cases:
- Marketing / QR codes that open the installed app to a report type or onboarding step.
An installed PWA may register file handlers via file_handlers, but MDN notes it is currently limited (Chromium-based, desktop OS). citeturn5search1turn5search14turn5search18turn5search26
Astrology use cases (desktop):
- Open exported
.json“birth chart profile” files or saved report PDFs (depending on your product design).
Screen Wake Lock lets you request the device keep screen on while the app is visible (useful for long report reading or guided sessions). citeturn12search0turn12search4turn4view0
Orientation lock exists but has limited availability; implement only with feature detection and graceful fallback. citeturn12search1turn12search21turn12search33
Content indexing lets you register offline-ready pages/content so the browser can surface them to users as available offline (Chrome has supported it). citeturn12search26turn12search2
Astrology mapping:
- When a report is “saved offline,” add it to the content index (where supported) to help discovery.
Background Fetch supports managing long downloads in the background but is experimental/limited; use only if you truly have large assets. citeturn12search3turn12search7turn12search19turn12search31
Passkeys on the web use WebAuthn to provide strong authentication. citeturn7search1turn7search5turn7search13
Astrology mapping:
- Reduce login friction for returning users and improve account security without native SDKs.
Payment Request API aims to simplify checkout flows; Safari supports Payment Request specifically for Apple Pay as documented by WebKit. citeturn7search2turn7search30turn7search6
Astrology mapping:
- Subscription upgrades without forcing traditional checkout form UX (availability varies; implement fallback checkout). citeturn7search2turn7search34turn7search30
- Installability requirements include serving over HTTPS (or localhost/loopback in dev). citeturn10search2turn1search24
- Service workers require secure contexts; otherwise registration fails. citeturn10search27turn10search3turn2search0
On iOS Home Screen web apps, requesting push permission must be tied to direct user interaction (a “subscribe” button), and permissions are managed per web app in Settings similarly to native apps. citeturn4view0
Agent must avoid auto-prompting on first load; instead request after a clear value moment (e.g., user enables reminders).
Offline experience should use:
- Cache API for assets/responses. citeturn2search12
- IndexedDB for structured offline data. citeturn8search1turn8search5
- Persistent storage request where possible (
navigator.storage.persist) to reduce eviction risk. citeturn8search0turn8search21turn8search23
Use browser DevTools “Application” tooling to inspect manifest, service workers, caches, simulate offline/push, etc. citeturn10search9turn10search11
manifest.json(or.webmanifest) at site root, linked from HTML. citeturn0search12turn6search8- Icon set:
192,512, plus maskable variant recommended. citeturn0search12turn4view0 sw.jsbuilt using Workbox injectManifest pattern (not generateSW if push/complex logic needed). citeturn2search3turn2search7offline.htmlfallback route and corresponding SW catch handler. citeturn2search4turn2search24- Install UX:
- Android/Chromium: custom install CTA gated by
beforeinstallpromptsupport. citeturn0search5turn0search1turn0search13 - iOS: “Add to Home Screen” guidance based on display-mode detection. citeturn1search2turn6search2turn0search13
- Android/Chromium: custom install CTA gated by
- Push + reminders backend:
- Endpoint to provide VAPID public key.
- Endpoint to save/update subscriptions.
- Scheduler to send reminder pushes. citeturn9search5turn9search2turn13search2
- Client feature flags and progressive enhancement wrappers for each advanced API (badging/share/wakelock/etc.). citeturn3search1turn3search31turn12search0
-
Installable behaviour:
- Android/Chromium shows install affordance; custom install button prompts when available. citeturn0search5turn0search1turn10search14
- iOS install instructions match Apple’s documented Add to Home Screen flow. citeturn1search2turn4view1
-
Offline:
- App shell loads offline after first successful online load;
offline.htmlappears for navigations when offline and uncached. citeturn2search4turn2search24
- App shell loads offline after first successful online load;
-
Updates:
- A new deployment does not trap users on old app code indefinitely; update is detectable and user can refresh into new version.
-
Push notifications:
- Android: subscribe + receive push while app not open. citeturn13search2turn2search5
- iOS: when installed as a Home Screen web app, permission request occurs only from user gesture and push is delivered similarly to native notifications. citeturn4view0
-
Reminders:
- Reminder scheduling works cross-platform via server push scheduling (no reliance on experimental local scheduling). citeturn13search2turn7search0
If the codebase is Flutter Web rather than plain HTML/JS: Flutter’s generated service worker from flutter build web is deprecated, and the official guidance is to disable it (--pwa-strategy=none) and build your own service worker (or use Workbox). citeturn0search6turn0search14turn0search2