Browser Progressive Web Apps (PWA)
A Hosanna Browser app can also be shipped as an installable Progressive Web App.
The installed app still uses the Web DOM renderer, while Hosanna supplies live
viewport, orientation, and lifecycle behavior for the web-pwa platform. The
host application remains responsible for its product identity, launch choice,
artwork, offline message, network policy, and web hosting.
PWA build scaffolding is available in @tantawowa/hosanna-tools 3.16.0 and
newer.
Ownership
| Layer | Responsibility |
|---|---|
| Hosanna UI | Detect the installed browser runtime, expose web-pwa capabilities, use live viewport and DPR metrics, publish orientation/viewport changes, and provide web lifecycle state and launch URL. |
| Hosanna Tools | Generate the manifest, registration module, versioned service worker, validated icon copies, and offline fallback from explicit app inputs. |
| Host application | Choose the app and expression, own manifest identity, icons, offline presentation, platform-web CSS, API/media exclusions, and deployment policy. |
| Web host | Serve the static build over HTTPS, keep mutable PWA files revalidatable, protect network-only routes from SPA fallback, and apply the intended service-worker scope. |
index.html can remain a thin root-level Vite entry. Product styling and other
Browser-only assets should live under the application's platforms/browser boundary.
PWA generation is never enabled for packaged Tizen or webOS output.
Add the Browser Assets
A typical app-owned layout is:
platforms/browser/
icons/
icon-192.png
icon-512.png
icon-maskable-512.png
apple-touch-icon.png
offline.html
style.css
web-pwa-policy.ts
index.html
vite.config.ts
Provide at least 192x192 and 512x512 install icons. A separate maskable icon is
recommended for launchers that crop artwork. When an icon declares
type: 'image/png' and a concrete size, the build verifies that the source is a
valid PNG with matching dimensions.
The offline page should be a small, self-contained explanation of what the user can do next. It is a navigation fallback, not a promise that application data or streaming media is available offline.
Configure Vite
Keep the product-specific inputs in the app's platform-web policy:
import type { HosannaPwaPluginOptions } from '@tantawowa/hosanna-tools/vite';
export const webPwaPolicy: HosannaPwaPluginOptions = {
manifest: {
id: '/',
name: 'Example',
shortName: 'Example',
description: 'Watch and explore Example.',
startUrl: '/?app=example&expression=phone',
scope: '/',
display: 'standalone',
orientation: 'any',
backgroundColor: '#f4f6f8',
themeColor: '#123456',
icons: [
{
src: '/pwa-icons/icon-192.png',
source: 'platforms/browser/icons/icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/pwa-icons/icon-512.png',
source: 'platforms/browser/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/pwa-icons/icon-maskable-512.png',
source: 'platforms/browser/icons/icon-maskable-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
{
src: '/pwa-icons/apple-touch-icon.png',
source: 'platforms/browser/icons/apple-touch-icon.png',
sizes: '180x180',
type: 'image/png',
purpose: 'any',
},
],
},
appleTouchIcon: '/pwa-icons/apple-touch-icon.png',
serviceWorker: {
offlineFallback: {
url: '/offline.html',
source: 'platforms/browser/offline.html',
},
networkOnly: ['/auth', '/media'],
cachePrefix: 'example-pwa',
},
};
Add the build-only plugin to vite.config.ts. Hosanna Tools intentionally
exposes a Vite-compatible structural plugin type without taking a dependency on
Vite itself. With Vite 6, keep the type assertion at this single package
boundary:
import { defineConfig, type Plugin } from 'vite';
import { hosannaPwaPlugin } from '@tantawowa/hosanna-tools/vite';
import { webPwaPolicy } from './platforms/browser/web-pwa-policy';
function asVitePlugin(plugin: unknown): Plugin {
return plugin as Plugin;
}
export default defineConfig({
plugins: [
asVitePlugin(hosannaPwaPlugin(webPwaPolicy)),
],
});
All public URLs must be root-relative and stay on the application origin.
Source files must be project-root-relative. The manifest startUrl must be
inside its scope, and appleTouchIcon must refer to one of the manifest
icons.
The main options are:
| Option | Purpose |
|---|---|
manifest.id, name, shortName, description | Installed-app identity and description. |
manifest.startUrl, scope, display, orientation | Launch URL and browser presentation contract. |
manifest.backgroundColor, themeColor, lang | Browser and launch-surface metadata. |
manifest.icons | Public src, local source, declared sizes, MIME type, and optional purpose. |
appleTouchIcon | Optional Apple home-screen icon; it must also appear in manifest.icons. |
serviceWorker.offlineFallback | Public output URL and local source for failed navigations. |
serviceWorker.networkOnly | Additional same-origin path prefixes that must bypass all caches. |
serviceWorker.cachePrefix | Product-specific prefix used for versioned cache names. |
manifestFilename, serviceWorker.filename, serviceWorker.registrationFilename | Optional output-name overrides for the manifest, worker, and registration module. |
The default output names are manifest.webmanifest, sw.js, and
pwa-register.js. The plugin copies the offline page and icons, then injects
the manifest, theme color, mobile-app metadata, Apple icon, and registration
module into the root HTML document.
Choose Launch Defaults in the App
Installation does not decide which product or layout expression to start.
Choose a stable startUrl in the manifest, including any app and expression
parameters that the product requires.
If a plain / discovery URL should use the same product defaults before the
app is installed, apply those defaults in the app's platform-web entry or app
initializer before calling getHosannaWebLaunchSearch. Hosanna deliberately
does not choose an app ID or expression for the host.
pwa=true (or pwa=1) is an explicit capability hint that selects the PWA
runtime while the app is open in a normal browser tab. An installed launch is
detected automatically through standalone display mode, including the iOS
standalone signal. pwa=false or pwa=0 explicitly keeps browser-web
capabilities even in a standalone host.
The expression remains an app-owned presentation choice. A PWA can use web,
phone, tablet, or tv; installation does not force a phone layout.
Installed Runtime Behavior
Without a device preset or explicit design dimensions, a PWA uses live
window.innerWidth and window.innerHeight as its Hosanna design surface. DPR
supplies the default physical DPI. Resize and physical-orientation changes
refresh the root scene and publish the current design resolution without
rebuilding an unchanged view tree.
Explicit emulator URLs retain their fixed contract. Supplying device,
designWidth, designHeight, displayWidth, displayHeight, width, or
height prevents the live viewport from replacing those dimensions.
The Web lifecycle service reports visible documents as active, hidden
documents as background, and exposes the full current URL as launch
information. Use the shared lifecycle and device services in product code
instead of adding direct document or window listeners to views.
See Browser Expression, DPI, and Touch Input for the complete geometry and input contract.
Cache and Update Policy
The generated worker applies a deliberately narrow cache policy:
- The offline page and manifest icons are precached.
- Navigations are network-first and fall back to the offline page only when the network fails.
- Only exact content-hashed files from the current Vite bundle are runtime-cached.
/api,/assets/meta/,/health, and/configuratorare network-only by default.- Streaming manifests, video, audio, segments, subtitles, and the app's additional
networkOnlyprefixes bypass caches. - Requests must be same-origin,
GET, and inside the configured scope before the worker considers them.
The cache version includes the manifest, offline assets, caching policy, and
emitted bundle names and contents. Activation removes older caches with the
same product prefix. The worker does not call skipWaiting or force a reload;
the app can decide when to offer an update.
The registration module dispatches these window events:
| Event | Detail |
|---|---|
hosanna:pwa-registration | Registration succeeded; detail.registration contains the ServiceWorkerRegistration. |
hosanna:pwa-update-available | A new worker reached installed while an older worker controls the page; detail.registration contains the registration. |
hosanna:pwa-registration-error | Registration failed; detail.error contains the error. |
Host the Build
Service workers require HTTPS in production; browsers also allow localhost for development. Serve the exact production build and apply these rules before the SPA fallback:
- Keep
/,/index.html,/offline.html,/manifest.webmanifest,/sw.js,/pwa-register.js, icons, and runtime config revalidatable rather than immutable. - Serve the manifest as
application/manifest+json. - Serve the worker from a path that covers the requested scope. If the hosting layout requires a broader scope, return the appropriate
Service-Worker-Allowedheader. - Give content-hashed Vite assets long-lived immutable caching.
- Route APIs, health checks, media, and other network-only endpoints before the static SPA fallback.
- Keep credentials and confidential config out of browser bundles, manifests, workers, and offline assets.
Verify Before Release
After the production build, serve its output through the release hosting configuration and verify:
- The manifest, registration module, worker, offline page, and every declared icon load with the intended MIME type and cache headers.
- The browser reports the app as installable and registers a worker with the expected scope.
- A reload is controlled by the worker and a failed navigation shows the offline page with its icon available.
- Hashed shell assets enter the runtime cache, while API, runtime-config, registration, and media URLs do not.
- Plain-browser, installed-window, resize, rotation, touch, keyboard, and explicit emulator launches keep the expected expression and dimensions.
- A new build reports an update without interrupting the currently controlled session.
Installability and an offline fallback do not replace normal Browser release tests. Continue to validate AppConfig, routes, networking, accessibility, media, storage, and target browsers.