Make the PWA installable as a standalone iOS home-screen app (#202)

iOS only grants Web Push to a site the user has added to the Home Screen via
Safari (16.4+), and it needs the apple-mobile-web-app meta tags to launch
standalone rather than in a Safari tab. Those were absent. Adds them to both
header families — the five standalone frontend pages and base.html.j2 — alongside
the existing theme-color:

  apple-mobile-web-app-capable, mobile-web-app-capable,
  apple-mobile-web-app-status-bar-style, apple-mobile-web-app-title

Adds ios-install.js: a dismissible hint shown only to real iOS Safari visitors
who haven't installed yet (skips Chrome/Firefox-iOS and standalone launches),
pointing them at Share -> Add to Home Screen and noting it's how to enable alerts.
There is no beforeinstallprompt on iOS, so a pointer to the control is the only
nudge available. The dismissal persists under the thermograph: localStorage
prefix — not tg:, which cache.js sweeps on every load.

No backend change: an installed iOS home-screen app reuses the existing service
worker and VAPID web-push stack as-is.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
This commit is contained in:
Emi Griffith 2026-07-19 17:40:04 -07:00 committed by GitHub
parent 9823fd47ec
commit f44e701b12
7 changed files with 136 additions and 0 deletions

View file

@ -6,6 +6,12 @@
<title>Thermograph — 2-year calendar</title> <title>Thermograph — 2-year calendar</title>
<meta name="description" content="A two-year calendar of daily weather grades for any location — see at a glance which days were unusually hot, cold, wet, or normal versus ~45 years of local climate history." /> <meta name="description" content="A two-year calendar of daily weather grades for any location — see at a glance which days were unusually hot, cold, wet, or normal versus ~45 years of local climate history." />
<meta name="theme-color" content="#f0803c" /> <meta name="theme-color" content="#f0803c" />
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Thermograph" />
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url / <!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
og:image origins in from this request's scheme://host + base path — og:image origins in from this request's scheme://host + base path —
crawlers need absolute URLs and don't run JS. --> crawlers need absolute URLs and don't run JS. -->
@ -112,5 +118,6 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="calendar.js"></script> <script type="module" src="calendar.js"></script>
<script type="module" src="ios-install.js"></script>
</body> </body>
</html> </html>

View file

@ -6,6 +6,12 @@
<title>Thermograph — compare comfort</title> <title>Thermograph — compare comfort</title>
<meta name="description" content="Compare how well two or more places hit your comfort temperature over any date range, side by side, each graded against its own ~45-year climate history." /> <meta name="description" content="Compare how well two or more places hit your comfort temperature over any date range, side by side, each graded against its own ~45-year climate history." />
<meta name="theme-color" content="#f0803c" /> <meta name="theme-color" content="#f0803c" />
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Thermograph" />
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url / <!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
og:image origins in from this request's scheme://host + base path — og:image origins in from this request's scheme://host + base path —
crawlers need absolute URLs and don't run JS. --> crawlers need absolute URLs and don't run JS. -->
@ -172,5 +178,6 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="compare.js"></script> <script type="module" src="compare.js"></script>
<script type="module" src="ios-install.js"></script>
</body> </body>
</html> </html>

View file

@ -6,6 +6,12 @@
<title>Thermograph — a single day</title> <title>Thermograph — a single day</title>
<meta name="description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." /> <meta name="description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." />
<meta name="theme-color" content="#f0803c" /> <meta name="theme-color" content="#f0803c" />
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Thermograph" />
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url / <!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
og:image origins in from this request's scheme://host + base path — og:image origins in from this request's scheme://host + base path —
crawlers need absolute URLs and don't run JS. --> crawlers need absolute URLs and don't run JS. -->
@ -78,5 +84,6 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="day.js"></script> <script type="module" src="day.js"></script>
<script type="module" src="ios-install.js"></script>
</body> </body>
</html> </html>

73
static/ios-install.js Normal file
View file

@ -0,0 +1,73 @@
// A small, dismissible "Add to Home Screen" hint for iPhone/iPad visitors.
//
// iOS only grants Web Push to a site the user has added to the Home Screen via
// Safari's Share sheet (iOS 16.4+). There is no beforeinstallprompt on iOS, so
// the only nudge available is telling people where the control is. This module
// shows that nudge — and only when it can actually help: real iOS Safari, not
// already installed, not previously dismissed. Everything degrades to nothing.
// Not the "tg:" prefix — cache.js sweeps that namespace on every load. The
// durable app prefer­ences ("thermograph:unit", "thermograph:loc") use this one.
const KEY = "thermograph:ios-a2hs";
function isIOS() {
const ua = navigator.userAgent || "";
// iPadOS 13+ reports as "Macintosh"; a touch point count disambiguates it.
return /iphone|ipad|ipod/i.test(ua) ||
(/Macintosh/.test(ua) && (navigator.maxTouchPoints || 0) > 1);
}
function isStandalone() {
return window.navigator.standalone === true ||
(window.matchMedia && window.matchMedia("(display-mode: standalone)").matches);
}
function isSafari() {
// Add to Home Screen only works from Safari proper — not Chrome (CriOS),
// Firefox (FxiOS), Edge (EdgiOS), Opera (OPiOS), or most in-app webviews.
const ua = navigator.userAgent || "";
if (/crios|fxios|edgios|opios/i.test(ua)) return false;
return /safari/i.test(ua);
}
function dismissed() {
try { return localStorage.getItem(KEY) === "off"; } catch (e) { return false; }
}
function remember() {
try { localStorage.setItem(KEY, "off"); } catch (e) {}
}
function build() {
const el = document.createElement("div");
el.className = "ios-a2hs";
el.setAttribute("role", "note");
el.setAttribute("aria-label", "Install Thermograph on your Home Screen");
// The share glyph mirrors iOS's own Share icon so the instruction is scannable.
el.innerHTML =
'<svg class="ios-a2hs-share" viewBox="0 0 24 24" aria-hidden="true" fill="none" ' +
'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M12 3v12"/><path d="M8 7l4-4 4 4"/>' +
'<path d="M5 12v7a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7"/></svg>' +
'<div class="ios-a2hs-text"><b>Add Thermograph to your Home Screen</b>' +
'<span>Tap Share, then <b>Add to Home Screen</b> — its how to get weather alerts on iPhone.</span></div>' +
'<button type="button" class="ios-a2hs-x" aria-label="Dismiss">×</button>';
el.querySelector(".ios-a2hs-x").addEventListener("click", () => {
remember();
el.remove();
});
return el;
}
function maybeShow() {
if (!isIOS() || isStandalone() || !isSafari() || dismissed()) return;
if (document.querySelector(".ios-a2hs")) return;
document.body.appendChild(build());
}
// Wait a beat so the hint never competes with first paint, and only on an
// interactive document.
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => setTimeout(maybeShow, 1200));
} else {
setTimeout(maybeShow, 1200);
}

View file

@ -6,6 +6,12 @@
<title>Thermograph — reading the grades</title> <title>Thermograph — reading the grades</title>
<meta name="description" content="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." /> <meta name="description" content="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." />
<meta name="theme-color" content="#f0803c" /> <meta name="theme-color" content="#f0803c" />
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Thermograph" />
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url / <!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
og:image origins in from this request's scheme://host + base path — og:image origins in from this request's scheme://host + base path —
crawlers need absolute URLs and don't run JS. --> crawlers need absolute URLs and don't run JS. -->
@ -109,5 +115,6 @@
document.getElementById("temp-scale").innerHTML = SCALE_TEMP.map(row).join(""); document.getElementById("temp-scale").innerHTML = SCALE_TEMP.map(row).join("");
document.getElementById("rain-scale").innerHTML = SCALE_RAIN.map(row).join(""); document.getElementById("rain-scale").innerHTML = SCALE_RAIN.map(row).join("");
</script> </script>
<script type="module" src="ios-install.js"></script>
</body> </body>
</html> </html>

View file

@ -2034,3 +2034,31 @@ details.hub-country > summary:hover { color: var(--accent); }
} }
.digest-note { margin: 0; font-size: 12px; } .digest-note { margin: 0; font-size: 12px; }
.digest-status { margin: 0; font-size: 14px; color: var(--accent); } .digest-status { margin: 0; font-size: 14px; color: var(--accent); }
/* iOS "Add to Home Screen" hint (frontend/ios-install.js) a slim dismissible
card pinned above the home indicator, shown only to iOS Safari visitors who
haven't installed yet. Themed like the rest of the surface chrome. */
.ios-a2hs {
position: fixed; z-index: 950;
left: 50%; transform: translateX(-50%);
bottom: calc(12px + env(safe-area-inset-bottom));
width: min(440px, calc(100vw - 24px));
display: flex; align-items: center; gap: 12px;
padding: 12px 14px;
background: var(--surface); color: var(--text);
border: 1px solid var(--border); border-radius: 14px;
box-shadow: 0 8px 30px rgba(0, 0, 0, .35);
animation: ios-a2hs-in .25s ease-out;
}
@keyframes ios-a2hs-in { from { opacity: 0; transform: translate(-50%, 12px); } }
.ios-a2hs-share { width: 26px; height: 26px; flex: 0 0 auto; color: var(--accent); }
.ios-a2hs-text { display: flex; flex-direction: column; gap: 2px; font-size: 13px; line-height: 1.3; }
.ios-a2hs-text b { font-weight: 700; }
.ios-a2hs-text span { color: var(--muted); }
.ios-a2hs-x {
flex: 0 0 auto; margin-left: auto;
width: 32px; height: 32px; border-radius: 8px;
background: none; border: none; color: var(--muted);
font-size: 22px; line-height: 1; cursor: pointer;
}
.ios-a2hs-x:hover { color: var(--text); background: var(--surface-2); }

View file

@ -6,6 +6,12 @@
<title>Thermograph — weather alerts</title> <title>Thermograph — weather alerts</title>
<meta name="description" content="Subscribe to a city and get notified when its weather is unusually extreme — above the percentile you choose." /> <meta name="description" content="Subscribe to a city and get notified when its weather is unusually extreme — above the percentile you choose." />
<meta name="theme-color" content="#f0803c" /> <meta name="theme-color" content="#f0803c" />
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Thermograph" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="Thermograph" /> <meta property="og:site_name" content="Thermograph" />
<meta property="og:title" content="Thermograph — weather alerts" /> <meta property="og:title" content="Thermograph — weather alerts" />
@ -58,5 +64,6 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" src="subscriptions.js"></script> <script type="module" src="subscriptions.js"></script>
<script type="module" src="ios-install.js"></script>
</body> </body>
</html> </html>