From de6de3c6c4f7fef59f97e58e66f0d75212397d8f Mon Sep 17 00:00:00 2001 From: desfoto automation Date: Fri, 25 Sep 2026 20:02:16 +0200 Subject: [PATCH] fix: cache canonical GTIN page without redirects --- README.md | 4 +++- scripts/build-site.py | 22 ++++++++++++++++++++++ site/gtin/sw.js | 12 +++++------- src/gtin/sw.js | 12 +++++------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e1871c1..a87ddd1 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ Werkzeug selbst verlinkt auf seine eigenständigen Rechtstexte unter `/gtin/impr - **GTIN-Generator.** Der Generator unter `/gtin/` rechnet vollständig im Browser, ohne Konto, externe API oder Serverübertragung. Er speichert bis zu 20 zuletzt erzeugte Einträge lokal im Browser; der Offline-Appcache enthält nur die - Anwendungsdateien. Die eigenständige Datenschutzerklärung liegt unter + Anwendungsdateien. Der Service Worker speichert die kanonische Startadresse + `/gtin/` ohne Weiterleitung; jede Änderung am Werkzeug erhält eine neue + Cache-Version. Die eigenständige Datenschutzerklärung liegt unter `/gtin/datenschutz/`; die Rechtstexte sind nicht mit denen des Hauptauftritts verknüpft. ## Herkunft der Inhalte diff --git a/scripts/build-site.py b/scripts/build-site.py index f056ef4..1827eb9 100755 --- a/scripts/build-site.py +++ b/scripts/build-site.py @@ -14,6 +14,7 @@ from __future__ import annotations import base64 import datetime as dt +import hashlib import io import json import shutil @@ -285,6 +286,7 @@ def build_html(table: dict[str, dict]) -> list[tuple[str, str]]: print(f" [html] {rel}") shutil.rmtree(SITE / "gtin", ignore_errors=True) shutil.copytree(SRC / "gtin", SITE / "gtin") + version_gtin_service_worker() print(" [html] gtin/ (unlisted browser tool and standalone legal pages)") not_found = pages.not_found().replace("og-desfoto.jpg", "og-desfoto.jpg") (SITE / "404.html").write_text(not_found, encoding="utf-8") @@ -292,6 +294,26 @@ def build_html(table: dict[str, dict]) -> list[tuple[str, str]]: return written +def version_gtin_service_worker() -> None: + """Give each GTIN source release its own offline cache.""" + digest = hashlib.sha256() + source_root = SRC / "gtin" + for path in sorted(source_root.rglob("*")): + if not path.is_file(): + continue + digest.update(path.relative_to(source_root).as_posix().encode("utf-8")) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + + worker = SITE / "gtin" / "sw.js" + script = worker.read_text(encoding="utf-8") + marker = "__BUILD_VERSION__" + if script.count(marker) != 1: + raise RuntimeError("GTIN service worker cache version marker is missing or duplicated") + worker.write_text(script.replace(marker, digest.hexdigest()[:12]), encoding="utf-8") + + def build_meta_files(routes: list[tuple[str, str]]) -> None: today = dt.date.today().isoformat() urls = [] diff --git a/site/gtin/sw.js b/site/gtin/sw.js index d732e3b..6811cfb 100644 --- a/site/gtin/sw.js +++ b/site/gtin/sw.js @@ -10,14 +10,13 @@ const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self)); -const CACHE_VERSION = '15d23ab0263e'; +const CACHE_VERSION = '403b5842d1cd'; const CACHE_PREFIX = 'desfoto-gtin-generator-'; const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`; // PRECACHE const PRECACHE = [ './', - './index.html', './css/app.css', './js/app.js', './js/gtin.js', @@ -36,7 +35,7 @@ function scopeUrl(pfad) { return new URL(pfad, sw.registration.scope).toString(); } -const INDEX_URL = scopeUrl('./index.html'); +const INDEX_URL = scopeUrl('./'); /** Pfad des Geltungsbereichs, z. B. "/gtin/". */ const SCOPE_PFAD = new URL(sw.registration.scope).pathname; @@ -59,7 +58,7 @@ const SHELL_KENNUNG = 'id="artikelnummer"'; * @returns {Promise} */ async function istAnwendungshuelle(antwort) { - if (!antwort.ok || antwort.type !== 'basic') return false; + if (!antwort.ok || antwort.type !== 'basic' || antwort.redirected) return false; const inhaltstyp = antwort.headers.get('content-type') ?? ''; if (!inhaltstyp.includes('text/html')) return false; const endgueltig = new URL(antwort.url); @@ -147,8 +146,7 @@ sw.addEventListener('fetch', (ereignis) => { if (anfrage.mode === 'navigate') { const appStart = new URL('./', sw.registration.scope).pathname; - const appIndex = new URL('./index.html', sw.registration.scope).pathname; - if (ziel.pathname !== appStart && ziel.pathname !== appIndex) return; + if (ziel.pathname !== appStart) return; ereignis.respondWith(bedieneNavigation(ereignis, anfrage)); return; } @@ -157,7 +155,7 @@ sw.addEventListener('fetch', (ereignis) => { caches.match(anfrage).then((treffer) => { if (treffer) return treffer; return fetch(anfrage).then((antwort) => { - if (antwort.ok && antwort.type === 'basic') { + if (antwort.ok && antwort.type === 'basic' && !antwort.redirected) { const kopie = antwort.clone(); ereignis.waitUntil( caches diff --git a/src/gtin/sw.js b/src/gtin/sw.js index d732e3b..fbc2ae1 100644 --- a/src/gtin/sw.js +++ b/src/gtin/sw.js @@ -10,14 +10,13 @@ const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self)); -const CACHE_VERSION = '15d23ab0263e'; +const CACHE_VERSION = '__BUILD_VERSION__'; const CACHE_PREFIX = 'desfoto-gtin-generator-'; const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`; // PRECACHE const PRECACHE = [ './', - './index.html', './css/app.css', './js/app.js', './js/gtin.js', @@ -36,7 +35,7 @@ function scopeUrl(pfad) { return new URL(pfad, sw.registration.scope).toString(); } -const INDEX_URL = scopeUrl('./index.html'); +const INDEX_URL = scopeUrl('./'); /** Pfad des Geltungsbereichs, z. B. "/gtin/". */ const SCOPE_PFAD = new URL(sw.registration.scope).pathname; @@ -59,7 +58,7 @@ const SHELL_KENNUNG = 'id="artikelnummer"'; * @returns {Promise} */ async function istAnwendungshuelle(antwort) { - if (!antwort.ok || antwort.type !== 'basic') return false; + if (!antwort.ok || antwort.type !== 'basic' || antwort.redirected) return false; const inhaltstyp = antwort.headers.get('content-type') ?? ''; if (!inhaltstyp.includes('text/html')) return false; const endgueltig = new URL(antwort.url); @@ -147,8 +146,7 @@ sw.addEventListener('fetch', (ereignis) => { if (anfrage.mode === 'navigate') { const appStart = new URL('./', sw.registration.scope).pathname; - const appIndex = new URL('./index.html', sw.registration.scope).pathname; - if (ziel.pathname !== appStart && ziel.pathname !== appIndex) return; + if (ziel.pathname !== appStart) return; ereignis.respondWith(bedieneNavigation(ereignis, anfrage)); return; } @@ -157,7 +155,7 @@ sw.addEventListener('fetch', (ereignis) => { caches.match(anfrage).then((treffer) => { if (treffer) return treffer; return fetch(anfrage).then((antwort) => { - if (antwort.ok && antwort.type === 'basic') { + if (antwort.ok && antwort.type === 'basic' && !antwort.redirected) { const kopie = antwort.clone(); ereignis.waitUntil( caches