From 4be934ccc1b0fd4975aa1fd0824a6362c9da928d Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Mon, 17 Aug 2026 22:37:52 +0200 Subject: [PATCH] feat(pwa): the PNG icons the manifest and iOS have always wanted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest pointed at icon.svg and nothing else. Chrome and Firefox install happily on that; Safari does not read the manifest's icons at all and looks for /apple-touch-icon.png by name, so an installed PWA on iOS got a screenshot of the page as its home-screen icon. This was written down as blocked: there is no SVG renderer on this machine, which is why the references were removed rather than left pointing at files that did not exist. Playwright is an SVG renderer, and it has been a dependency the whole time. scripts/generate-icons.js renders the same SVG at 192, 512 and 180. A script rather than a test, so CI never runs it and never rewrites a tracked binary — run it when the icon changes. The smoke test already fetches every icon the manifest names, and now asks for the Apple one by name too, so a reference with no file behind it fails there rather than on somebody's phone. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 6 +++++ web/scripts/generate-icons.js | 40 ++++++++++++++++++++++++++++++++ web/src/app.html | 4 ++++ web/static/apple-touch-icon.png | Bin 0 -> 3634 bytes web/static/icon-192.png | Bin 0 -> 3734 bytes web/static/icon-512.png | Bin 0 -> 11050 bytes web/static/manifest.webmanifest | 20 +++++++++++++++- 7 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 web/scripts/generate-icons.js create mode 100644 web/static/apple-touch-icon.png create mode 100644 web/static/icon-192.png create mode 100644 web/static/icon-512.png diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index cbfa9dd..70fe2e9 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -183,6 +183,12 @@ test('manifestet og service workeren findes, og ikonerne findes også', async ({ expect(iconResponse.ok(), `${icon.src} findes ikke`).toBeTruthy(); } + // Safari never reads the manifest's icons; it looks for this by name, and + // without it an installed PWA on iOS gets a screenshot of the page as its + // home-screen icon. + const appleIcon = await request.get('/apple-touch-icon.png'); + expect(appleIcon.ok(), 'apple-touch-icon.png findes ikke').toBeTruthy(); + // The service worker is what Web Push needs; without it a subscription cannot // be created at all. const worker = await request.get('/sw.js'); diff --git a/web/scripts/generate-icons.js b/web/scripts/generate-icons.js new file mode 100644 index 0000000..2674b61 --- /dev/null +++ b/web/scripts/generate-icons.js @@ -0,0 +1,40 @@ +/** + * Renders static/icon.svg to the PNGs the manifest and iOS need. + * + * node scripts/generate-icons.js + * + * A script rather than a test, so CI never runs it and never rewrites a tracked + * binary. Run it when the icon changes; the smoke test fetches every icon the + * manifest names, so a reference with no file behind it fails there. + * + * It drives Chromium because there is no SVG renderer on this machine — which is + * why these references were once removed rather than left pointing at files that + * did not exist. Playwright is a renderer, and it is already a dependency. + */ +import { chromium } from '@playwright/test'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const SIZES = [ + // The two the manifest asks for: 192 for a launcher, 512 for a splash screen. + { file: 'static/icon-192.png', size: 192 }, + { file: 'static/icon-512.png', size: 512 }, + // And the one Safari wants by name. It ignores the manifest's icons entirely + // and looks for this, which is why an installed PWA on iOS otherwise gets a + // screenshot of the page as its home-screen icon. + { file: 'static/apple-touch-icon.png', size: 180 } +]; + +const svg = readFileSync('static/icon.svg', 'utf8'); +const browser = await chromium.launch(); +const page = await browser.newPage(); + +for (const { file, size } of SIZES) { + await page.setViewportSize({ width: size, height: size }); + await page.setContent( + `${svg}` + ); + writeFileSync(file, await page.locator('svg').screenshot()); + console.log(`${file} ${size}×${size}`); +} + +await browser.close(); diff --git a/web/src/app.html b/web/src/app.html index f913c5d..3e8fe18 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -7,6 +7,10 @@ + + verdande