Make it installable
Your app can be installed from the browser to a phone's home screen or a computer, and open like an app. It takes one switch, a name and one icon.
What people see, the update bar and the install numbers are in Installable app (PWA). This page is the setup.
Turn it on
-
Switch on the feature in
src/custom/features.ts. The PWA is the one feature that is off until you ask for it:export const CUSTOM_FEATURES: FeatureChoice = { on: ['pwa'], }; -
Name it in
src/custom/pwa.ts. Anything you leave out keeps Arc CMS's default; every key and default is in Installable app (PWA):export const CUSTOM_PWA: Partial<PwaConfig> = { name: 'Sanskrit for Kids', // install dialog and app switcher shortName: 'Sanskrit', // under the home screen icon, about 12 characters themeColor: '#e8590c', // browser bar and phone status bar startUrl: '/learn', // the page the home screen icon opens }; -
Add one icon: a square image of at least 512 by 512 pixels, saved as
src/custom/pwa-icon.svgorsrc/custom/pwa-icon.png. Keep the important part inside the middle 80%, since Android crops it into a circle or rounded square.An app built for several brands keeps one icon per brand and names the one to use in
pwa.ts, with its path from the project folder:export const CUSTOM_PWA: Partial<PwaConfig> = { name: 'Acme Lessons', icon: 'src/custom/brands/acme/icon.svg', }; -
Build and deploy the website. These settings are read at build time, so every change needs a new build and a hosting deploy. See Deploy.
Putting enabled in src/custom/pwa.ts stops the build with a message. The switch lives only in src/custom/features.ts.
What the build makes
| File | From |
|---|---|
| The web app manifest | Your name, short name, description, colors and start page, shown full screen (standalone) with the whole site as its scope |
| Every icon size | Your one image: the standard app icon sizes, the maskable icon, and the 180 by 180 pixel iPhone icon |
The service worker, sw.js | Stores the app's main code, styles and fonts; pages always come from the network first, and the stored app opens when there is no connection |
| iPhone tags | The home screen icon link and the short name as the iPhone app title |
With the feature off, none of these are built, and a visitor who still has an old service worker gets it removed on their next visit.
Show the install button
Arc CMS shows the install card at the top of the member dashboard. To show it on a page of your own, such as your app's start page, add <arc-install-prompt /> (see Installable app (PWA)). It shows what fits the browser, and nothing once the app is installed.
On your published home page, give any button data-arc-install hidden: it appears when the browser offers installing and asks on a click (see Build your home page).
Build your own install card
<arc-install-prompt /> carries its own styles and icons, so it also works inside Shadow DOM. To draw your own sheet instead, in your app's look, use PwaService (src/app/core/pwa/pwa.service.ts). Its install side is supported and stable:
| Member | What it does |
|---|---|
showInstall() | A signal: true when the card should show (the browser can install, the app is not installed, the PWA is on, and the person has not chosen Not now in the last 30 days). |
installMode() | A signal: how this browser installs, or null. 'prompt' (Android, desktop Chrome and Edge: call install()), 'ios-safari' (Safari on iPhone and iPad: Share, then Add to Home Screen), 'ios-browser' (Chrome, Edge, Firefox and others on iPhone and iPad, iOS 16.4 and later: the same steps from that browser's Share button, often in its address bar), 'ios-other' (a web view inside another app, or an older iOS: the page must be opened in Safari). |
install() | Opens the browser's own install dialog, for 'prompt'. Does nothing otherwise. |
dismiss() | Not now: showInstall() turns false on this device for 30 days, and the decline is counted. |
shown() | Call it when your card comes into view, so the install numbers count it (once per device). |
installed() | A signal: the app runs from the home screen or as an installed desktop app. |
readonly pwa = inject(PwaService);
constructor() {
effect(() => { if (this.pwa.showInstall()) this.pwa.shown(); });
}
// template: @if (pwa.showInstall()) { ... @switch (pwa.installMode()) { ... } }
Test that it installs
On your computer: build, serve the build, and check Chrome's DevTools, Application, as described in Installable app (PWA).
npm run devhas no service worker, so it cannot be used for this.On an Android phone: deploy to your Firebase Hosting site, open it in Chrome, and install from the install card or the browser menu. The icon, name and splash colors come from your settings.
On an iPhone: open the same address in Safari and add it to the home screen (below).
After a change: build and deploy again, then reopen the installed app. It offers the new version with an Update button instead of reloading by itself.
Show the update yourself
By default Arc CMS shows its own "A new version is ready" bar, and holds it back on full-screen pages until a normal page opens. An app that lives entirely on full-screen pages (a kiosk, a check-in screen) would never see it. Such an app shows the update where it suits, such as a lock screen, and Arc CMS stays out of the way.
Tell Arc CMS the page owns the update. Add
pwaUpdate: 'app'to the route'sdata. Every page below that route inherits it, and Arc CMS's own bar is not shown there at all.'app'is the only value; any other is ignored.{ path: 'lock', loadComponent: () => import('./lock.component').then((m) => m.default), data: { fullScreen: true, pwaUpdate: 'app' } }Use
PwaService(src/app/core/pwa/pwa.service.ts) in that page:Member What it does updateReady()A read-only signal, true while a new version is waiting. Always false when the pwafeature is off.checkForUpdate()Looks for a new version now, instead of waiting for the hourly check. It resolves once the browser has looked, and does nothing when the feature is off, offline, or before the service worker has registered. applyUpdate()Switches to the waiting version and reloads the page. It does nothing when no update is waiting.
import { Component, inject } from '@angular/core';
import { PwaService } from '../app/core/pwa/pwa.service'; // from src/custom/lock.component.ts
@Component({
selector: 'app-lock',
template: `
@if (pwa.updateReady()) {
<button type="button" (click)="pwa.applyUpdate()">Update now</button>
}
`,
})
export default class LockComponent {
readonly pwa = inject(PwaService);
constructor() {
void this.pwa.checkForUpdate(); // the lock screen is a quiet moment to look
}
}
Arc CMS never calls applyUpdate() itself, on any page, so a page is never reloaded in the middle of a task. Call it only when the person is at a safe point. On a page without the flag, nothing changes: the bar shows as before.
When another tab updates
The app may be open in more than one tab or window. When someone updates in one of them, the new version takes over every open tab. A normal page reloads into it, as before. A page with pwaUpdate: 'app' does not reload: updateReady() stays true (or turns true), and your applyUpdate() reloads it into the new version at the next safe point. Until then the page keeps running the old version and may not open a screen it has not opened yet (stale(), below), so offer the update soon.
Do not leave an update waiting for long. A deploy replaces the old version's code files on the server, so a page of the old version that has not opened a screen yet cannot load it until the update is applied, unless its code was stored ahead (below). Offer the update at the next quiet moment, such as the lock screen.
Open screens offline and after a deploy
The service worker stores the code every page needs when the app installs. The code of each screen is stored the first time it opens. So a screen never opened on this device cannot open offline, nor after a deploy has removed the old version's files from the server. An app that must open every screen, such as a kiosk used through the day, has them stored ahead in src/custom/pwa.ts:
export const CUSTOM_PWA: Partial<PwaConfig> = {
routeCode: 'app',
};
routeCode | Stored when the app installs or updates | Size, Arc CMS on its own |
|---|---|---|
visited (the default) | What every page needs; each screen's code when it first opens | About 1.8 MB |
app | Also every screen outside the admin area, with everything it uses. Chosen from the build's own code graph, so your pages are found by themselves; the admin's editors and settings are left out, except code public pages share | About 3.8 MB |
all | Every screen, the admin's too, for an app whose admins also work offline | About 6.6 MB |
The build prints what it stores ("PWA: 110 code files, 3.8 MB stored ...") and warns above 15 MB. Your own pages add to the sizes above. Data is a separate matter: to show data offline too, turn on the offline cache (see Keep working offline).
Start offline after an update
The service worker keeps a copy of each page opened, for offline starts. After an update, the copies made by the old version point at code files the update removed, so they would open blank. Arc CMS checks a stored page before it opens it: if a code file it starts with is no longer on the device, the new version's app opens instead, at the same address. Published pages, which use no app code, open from their stored copy as before. A screen whose code was never stored is the case below.
A page waits 4 seconds for the network before it uses a stored copy. On a network without internet, such as a kiosk on a closed Wi-Fi, every start waits that long. Lower it in src/custom/pwa.ts, from 1 to 30 seconds:
export const CUSTOM_PWA: Partial<PwaConfig> = {
navigationTimeoutSeconds: 2,
};
When a screen's code is missing anyway
Without the PWA, or with a screen that was not stored, a tab left open across a deploy can still meet a missing code file. Arc CMS then loads the address the person asked for afresh, so the tap opens that screen in the new version. If that fails as well within 10 seconds, a short message with a Reload button shows instead of trying again. On a page with data: { pwaUpdate: 'app' }, Arc CMS never reloads: StaleCodeService (src/app/core/version/stale-code.service.ts) sets stale(), and your page decides when to call reload(), as it does for updates.
iPhone and iPad
- Safari, and since iOS 16.4 Chrome, Edge and Firefox too, add the app to the home screen from their Share menu. The install card shows the two steps (tap Share, then "Add to Home Screen") in each of them. Only a web view inside another app (Instagram, Facebook, the Google app), or an older iOS, gets "open this page in Safari", with a Copy link button.
- iPhone reports no install event, so the install numbers count the first opening from the home screen as the install.
- The home screen uses the iPhone icon and the short name, both made from your settings. The icon is padded onto a white tile unless
appleIcon: 'fill'insrc/custom/pwa.tsmakes a full-square icon fill it (see Installable app (PWA)).
Wrap it for the app stores
To publish in the App Store or Google Play, wrap the installable app with Capacitor: add Capacitor to the project, point its web directory at the browser files that npm run build writes, add the plugins your app needs (push notifications, camera and so on, chosen per app), and build the iOS and Android apps with Xcode and Android Studio. Follow Capacitor's own documentation for each step.