Filipe, 22.09.2026: "und nochmal, ich will einen installieren button.
damit die leute es viel einfacher haben die seite auf dem pc oder auf
dem handy zu installieren!!! bitte das hab ich auch schon paar mal
gefragt. mach das es ist seeeeeeeehr wichtig."
Er hatte recht, und der Knopf war seit dem 20.09. gebaut. Er konnte
nur nie erscheinen. ZWEI URSACHEN, beide ausserhalb dessen, was
geprueft wurde:
1. DER SERVICE WORKER LIEF NIE. Er wurde ausschliesslich in glocke.js
angemeldet -- also erst, wenn jemand Benachrichtigungen ERLAUBT.
Auf Filipes Rechner stehen sie auf "Vom Browser blockiert"; das
steht sogar in seiner Kopfleiste auf dem Bildschirmfoto von heute
frueh. Ohne Service Worker macht Chrome kein Installationsangebot.
2. UND AUCH MIT WAERE ES NICHT GEGANGEN: Chrome verlangt einen Service
Worker MIT `fetch`-Zuhoerer. Dieser hatte keinen.
Ohne Angebot kommt `beforeinstallprompt` nie, und der Knopf bleibt
`hidden`. Fuer immer.
WAS SICH GEAENDERT HAT
- Der Service Worker wird jetzt auf JEDER Seite angemeldet, unabhaengig
von Benachrichtigungen. Das gehoert zum Installieren, nicht zur
Glocke.
- sw.js bekommt einen `fetch`-Zuhoerer, der NICHTS ablegt. Das
Versprechen im Kopf der Datei ("bewusst ohne Zwischenspeicher, der
Workspace liegt hinter einer Anmeldung") bleibt damit wortwoertlich
gueltig: Er reicht Seitenaufrufe durch und baut nur dann selbst eine
Antwort, wenn das Netz weg ist. Keine Serverantwort wird aufgehoben.
- Der Knopf zeigt sich, sobald der Browser installieren KANN, statt
erst nach dem Angebot. Geprueft wird die Faehigkeit
(`'onbeforeinstallprompt' in window`), nicht der Name des Browsers.
- DIE ANMELDEWAND BEKOMMT IHN AUCH. Sie hat keine Kopfleiste und
deshalb bisher gar kein Angebot -- dabei ist sie die Seite, auf der
jeder zuerst landet. Genau die Leute, um die es Filipe geht.
- Dafuer steht der Knopf jetzt in einer eigenen Datei
(assets/js/installieren.js) statt in kopf.js, das die Wand nicht
laedt. Und sein Stil in gate.css statt in module.css -- vierter Fall
derselben Art nach .knopf-still, dem Schalter und .feld-hinweis.
- Ohne `nachfrage.js` (also auf der Wand) erklaert er den Weg als
Absatz unter sich. Der erste Entwurf rief `window.frageNach?.()`
auf: Auf dem iPhone steht der Knopf dort von Anfang an da, und er
haette beim Antippen stumm nichts getan.
- Der Aufruf haengt nicht mehr an der Reihenfolge der Skriptzeilen.
chat.html und leistung.html laden kopf.js OHNE `defer`, dort waere
installieren.js immer zu spaet gekommen -- zwei von 37 Seiten ohne
Knopf, ohne Meldung.
- Chromes Manifest-Warnung zum `share_target` behoben (enctype).
WARUM DIE PRUEFUNG DAS NICHT GEFUNDEN HAT -- und was jetzt anders ist
Sie war gruen, die ganze Zeit. Sie hat `beforeinstallprompt` SELBST
zugestellt und gemessen, ob der Knopf darauf reagiert. Die eine Frage,
auf die es ankam -- "bietet der Browser es ueberhaupt an?" -- hat sie
nie gestellt.
Jetzt fragt sie Chrome direkt (`Page.getInstallabilityErrors`, sein
eigenes Urteil) und misst die Voraussetzungen statt der Reaktion:
laeuft ein Service Worker, OBWOHL Benachrichtigungen auf "denied"
stehen; hat sw.js einen fetch-Zuhoerer; legt er wirklich nichts ab;
steht der Knopf auf der Wand und tut er dort auch etwas.
Gemessen, mit Benachrichtigungen auf "denied":
Service Worker: activated
Chromes Urteil: kein einziges Hindernis
Manifest: nichts beanstandet
Knopf: 124x45 px, sichtbar, genau einer
pruef-installieren 29/0 (von 14). Dazu gruen: pruef-css-klassen,
pruef-struktur, pruef-crew-adresse, pruef-start-ansicht.
Co-Authored-By: Claude Opus 5 <[email protected]>
274 lines
12 KiB
JavaScript
274 lines
12 KiB
JavaScript
/* =====================================================================
|
||
installieren.js — die Seite als App aufs Gerät holen
|
||
=====================================================================
|
||
|
||
ZWEI DINGE IN EINER DATEI, und sie gehoeren zusammen:
|
||
|
||
1. Der Service Worker wird angemeldet -- IMMER, auf jeder Seite.
|
||
2. Der Knopf, der das Installieren anbietet.
|
||
|
||
WARUM BEIDES HIER STEHT (22.09.2026, nach Filipes drittem Anlauf:
|
||
"ich will einen installieren button ... das hab ich auch schon paar
|
||
mal gefragt. mach das es ist seeeeeeeehr wichtig."):
|
||
|
||
Der Knopf gab es seit dem 20.09. Er war nur NIE ZU SEHEN. Chrome
|
||
zeigt sein Installationsangebot (`beforeinstallprompt`) erst, wenn
|
||
ein Service Worker laeuft -- und der wurde ausschliesslich in
|
||
`glocke.js` angemeldet, also erst, wenn jemand Benachrichtigungen
|
||
ERLAUBT. Auf Filipes Rechner stehen sie auf "Vom Browser blockiert".
|
||
Damit lief nie einer, damit kam das Ereignis nie, damit blieb der
|
||
Knopf `hidden`. Fuer immer.
|
||
|
||
Die Anmeldung gehoert also nicht an die Glocke, sondern an das
|
||
Installieren -- und beides an jede Seite. Deshalb eine Datei, die
|
||
ueberall geladen wird, statt einer Abhaengigkeit, die kaum jemand
|
||
erfuellt.
|
||
|
||
UND DESHALB LAEDT AUCH DIE ANMELDEWAND SIE. Sie hat keine
|
||
Kopfleiste und deshalb frueher kein Angebot bekommen -- dabei ist
|
||
sie die Seite, auf der jeder zuerst landet.
|
||
===================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* ---------------------------------------------------------------------
|
||
DER SERVICE WORKER -- ohne Bedingung, aber auch ohne Laerm
|
||
---------------------------------------------------------------------
|
||
Er scheitert an genug Stellen, an denen niemand etwas falsch
|
||
gemacht hat: Privates Fenster, abgeschaltete Worker,
|
||
Unternehmens-Richtlinie, veralteter Browser. Nichts davon ist ein
|
||
Fehler, den ein Besucher sehen soll -- die Seite funktioniert auch
|
||
ohne, sie laesst sich dann nur nicht installieren.
|
||
|
||
`load` abwarten: Die Anmeldung zieht Bandbreite, und zwar genau in
|
||
dem Moment, in dem die Seite noch laedt. */
|
||
if ('serviceWorker' in navigator) {
|
||
const anmelden = () => {
|
||
navigator.serviceWorker.register('/workspace/sw.js', { scope: '/workspace/' })
|
||
.catch(() => { /* geht hier nicht -- die Seite laeuft trotzdem */ });
|
||
};
|
||
if (document.readyState === 'complete') anmelden();
|
||
else window.addEventListener('load', anmelden, { once: true });
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE SEITE ALS APP INSTALLIEREN (20.09.2026)
|
||
|
||
Filipe: „kannst du da dazwischen in der starseite oben, ein button
|
||
reinsetzen damit die leute die seite installieren koennen auf dem
|
||
handy oder pc. das soll auch verschwinden wenn die leute es
|
||
installiert haben."
|
||
|
||
WARUM ES DEN KNOPF BRAUCHT: Das Manifest ist seit Langem
|
||
vollstaendig -- die App LIESS sich installieren. Nur bot das
|
||
ausschliesslich der Browser selbst an, versteckt in seinem
|
||
Dreipunktemenue. Wer es nicht sucht, findet es nie. Und ohne
|
||
installierte App gibt es auf dem Handy keine verlaesslichen
|
||
Benachrichtigungen; genau daran hing im September das Telefonieren.
|
||
|
||
DREI ZUSTAENDE, UND JEDER BRAUCHT EINE ANDERE ANTWORT:
|
||
|
||
1. SCHON INSTALLIERT -> gar kein Knopf. Ein Angebot fuer etwas,
|
||
das man hat, ist Rauschen. Erkannt an `display-mode:
|
||
standalone` (und an Safaris eigenem `navigator.standalone`).
|
||
|
||
2. DER BROWSER BIETET ES AN (Chrome, Edge, Android) -> der Knopf
|
||
fragt ihn. `beforeinstallprompt` kommt EINMAL und muss
|
||
aufgehoben werden; wer ihn nicht festhaelt, kann spaeter
|
||
nicht mehr fragen.
|
||
|
||
3. SAFARI AUF DEM iPHONE -> dort gibt es `beforeinstallprompt`
|
||
NICHT, und es wird ihn nicht geben. Apple laesst nur den Weg
|
||
ueber „Teilen -> Zum Home-Bildschirm". Ein Knopf, der dort
|
||
nichts tut, waere schlimmer als keiner -- also sagt er in
|
||
diesem Fall, was zu tun ist, statt es zu versuchen.
|
||
|
||
WARUM AUF JEDER SEITE UND NICHT NUR AUF DER STARTSEITE: Wer den
|
||
Knopf einmal uebersieht, sieht ihn beim naechsten Mal wieder -- und
|
||
er verschwindet ohnehin, sobald er ueberfluessig ist.
|
||
|
||
(Hier stand "auf allen 32 Seiten". Es sind inzwischen mehr, und
|
||
seit dem 22.09. auch die Anmeldewand. Eine gezaehlte Zahl in einem
|
||
Kommentar wird still falsch, sobald jemand eine Seite anlegt --
|
||
also steht jetzt keine mehr da.)
|
||
===================================================================== */
|
||
|
||
function knopfBauen(wohin) {
|
||
/* ZWEIMAL AUFGERUFEN HEISST NICHT ZWEI KNOEPFE. Seit der Aufruf
|
||
nicht mehr an der Reihenfolge der Skriptzeilen haengt, kann er
|
||
aus zwei Richtungen kommen -- aus kopf.js und aus dem Platz
|
||
unten in dieser Datei. Zwei Angebote fuer dieselbe Sache sind
|
||
eines zu viel. */
|
||
if (!wohin || wohin.querySelector('.install-knopf')) return;
|
||
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
|
||
/* ---- Zustand 1: schon installiert -> nichts tun --------------- */
|
||
const schonDrin = () =>
|
||
(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|
||
|| window.navigator.standalone === true;
|
||
if (schonDrin()) return;
|
||
|
||
/* Apple: kein `beforeinstallprompt`, aber ein Weg. */
|
||
const istApple = /iphone|ipad|ipod/i.test(navigator.userAgent)
|
||
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||
|
||
let angebot = null; // der aufgehobene Browser-Vorschlag
|
||
|
||
const knopf = el('button', 'install-knopf');
|
||
knopf.type = 'button';
|
||
knopf.hidden = true;
|
||
knopf.title = 'Diese Seite als App installieren';
|
||
/* DAS WORT VERSCHWINDET AM HANDY (siehe CSS) -- der Satz fuer
|
||
Vorleseprogramme nicht. Ein Knopf, der dort nur aus einem
|
||
Pfeil besteht, heisst sonst "Schaltflaeche". */
|
||
knopf.setAttribute('aria-label', 'Diese Seite als App installieren');
|
||
const zeichen = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
zeichen.setAttribute('viewBox', '0 0 24 24');
|
||
zeichen.setAttribute('class', 'install-knopf__zeichen');
|
||
zeichen.setAttribute('aria-hidden', 'true');
|
||
zeichen.innerHTML = '<path d="M12 3.5v11M8 11l4 4 4-4"/><path d="M5 18.5h14"/>';
|
||
knopf.append(zeichen, el('span', 'install-knopf__text', 'Installieren'));
|
||
|
||
knopf.addEventListener('click', async () => {
|
||
if (angebot) {
|
||
/* Zustand 2: den Browser fragen. Die Antwort interessiert uns
|
||
nur insofern, als ein angenommener Vorschlag den Knopf
|
||
ueberfluessig macht -- ein abgelehnter nicht: Vielleicht
|
||
ueberlegt man es sich morgen anders. */
|
||
knopf.disabled = true;
|
||
try {
|
||
angebot.prompt();
|
||
const { outcome } = await angebot.userChoice;
|
||
if (outcome === 'accepted') knopf.remove();
|
||
} catch { /* dann eben nicht */ }
|
||
angebot = null;
|
||
knopf.disabled = false;
|
||
return;
|
||
}
|
||
/* Zustand 3: erklaeren statt versuchen. */
|
||
erklaeren();
|
||
});
|
||
|
||
/* WARUM DAS NICHT EINFACH `frageNach` AUFRUFT (22.09.2026).
|
||
|
||
Der erste Entwurf tat genau das -- und haette auf der
|
||
Anmeldewand gar nichts getan: Sie laedt `nachfrage.js` nicht,
|
||
also ist `window.frageNach` dort `undefined`, und `?.()`
|
||
verschluckt das lautlos. Auf dem iPhone steht der Knopf aber von
|
||
Anfang an da. Ein Knopf, der beim Antippen nichts tut, ist
|
||
schlimmer als keiner.
|
||
|
||
Die Datei nachzuladen waere die falsche Antwort: 22 KB und die
|
||
halbe Stilvorlage `module.css` fuer eine Seite, die eine einzige
|
||
Aufgabe hat. Und ein Kasten, der sich ueber das Codefeld legt,
|
||
waere dort ohnehin im Weg.
|
||
|
||
Also: Wo es den Baustein gibt, wird er benutzt -- die Seite soll
|
||
eine Sprache sprechen. Wo nicht, steht die Erklaerung als
|
||
schlichter Absatz unter dem Knopf. Beides sagt dasselbe. */
|
||
let hinweis = null;
|
||
function erklaeren() {
|
||
const titel = 'Als App auf dem Home-Bildschirm';
|
||
const weg = istApple
|
||
? 'Tippe unten in Safari auf das Teilen-Zeichen (Viereck mit Pfeil nach oben), '
|
||
+ 'dann auf „Zum Home-Bildschirm".'
|
||
: 'Öffne das Menü deines Browsers (die drei Punkte) und wähle dort '
|
||
+ '„App installieren" oder „Zum Startbildschirm hinzufügen".';
|
||
const dazu = 'Danach liegt die Seite wie eine App auf deinem Gerät – und '
|
||
+ 'Anrufe und Nachrichten kommen zuverlässiger an.';
|
||
|
||
if (typeof window.frageNach === 'function') {
|
||
window.frageNach({ titel, was: weg, bleibt: dazu, ja: 'Alles klar', nein: null });
|
||
return;
|
||
}
|
||
|
||
/* Ohne Dialogbaustein: ein Absatz, der beim zweiten Antippen
|
||
wieder verschwindet. Kein zweiter, wenn schon einer dasteht. */
|
||
if (hinweis) { hinweis.remove(); hinweis = null; knopf.setAttribute('aria-expanded', 'false'); return; }
|
||
hinweis = el('p', 'install-hinweis');
|
||
hinweis.id = 'install-hinweis';
|
||
hinweis.append(el('strong', 'install-hinweis__titel', titel));
|
||
hinweis.append(el('span', 'install-hinweis__weg', weg));
|
||
hinweis.append(el('span', 'install-hinweis__dazu', dazu));
|
||
knopf.setAttribute('aria-expanded', 'true');
|
||
knopf.setAttribute('aria-controls', 'install-hinweis');
|
||
knopf.insertAdjacentElement('afterend', hinweis);
|
||
}
|
||
|
||
wohin.prepend(knopf);
|
||
|
||
/* Der Browser meldet sich, wenn er es anbieten kann. */
|
||
window.addEventListener('beforeinstallprompt', (e) => {
|
||
/* Ohne `preventDefault` zeigt der Browser seine eigene Leiste --
|
||
zwei Angebote fuer dieselbe Sache sind eines zu viel. */
|
||
e.preventDefault();
|
||
angebot = e;
|
||
knopf.hidden = false;
|
||
});
|
||
|
||
/* WANN DER KNOPF ZU SEHEN IST -- und warum nicht erst nach dem
|
||
Angebot des Browsers (geaendert 22.09.2026).
|
||
|
||
Bis heute wurde er ausschliesslich durch `beforeinstallprompt`
|
||
sichtbar. Das hat zwei Loecher:
|
||
|
||
* Apple schickt das Ereignis nie. (Das war bekannt und ist
|
||
unten schon abgefangen.)
|
||
* Chrome schickt es zwar, aber nicht verlaesslich sofort -- und
|
||
gar nicht, wenn es kurz vorher weggeklickt wurde. Dann steht
|
||
der Knopf da, wo er gebraucht wird, und ist unsichtbar.
|
||
|
||
Filipe hat dreimal danach gefragt. Ein Angebot, das man nur unter
|
||
guenstigen Umstaenden sieht, ist fuer ihn keins.
|
||
|
||
GEPRUEFT WIRD DIE FAEHIGKEIT, NICHT DER NAME DES BROWSERS:
|
||
`'onbeforeinstallprompt' in window` ist bei allen Chromium-
|
||
Browsern wahr -- Chrome, Edge, Brave, Samsung Internet, Android.
|
||
Eine Liste von Browsernamen waere die, die beim naechsten
|
||
veraltet.
|
||
|
||
Wo BEIDES nicht zutrifft -- Firefox am Rechner etwa, der
|
||
Webanwendungen schlicht nicht installiert --, bleibt der Knopf
|
||
weg. Ein Knopf, der eine Anleitung fuer etwas zeigt, das dieser
|
||
Browser nicht kann, ist schlimmer als keiner. */
|
||
const kannVielleicht = 'onbeforeinstallprompt' in window;
|
||
if (istApple || kannVielleicht) knopf.hidden = false;
|
||
|
||
/* Und wenn es geklappt hat, ist er fertig. */
|
||
window.addEventListener('appinstalled', () => knopf.remove());
|
||
/* Manche Browser melden `appinstalled` nicht -- dann faellt der
|
||
Knopf beim naechsten Oeffnen ueber `schonDrin()` weg. Und wer die
|
||
App waehrend der Sitzung installiert und zurueckwechselt, merkt
|
||
es hier: */
|
||
try {
|
||
window.matchMedia('(display-mode: standalone)')
|
||
.addEventListener('change', (m) => { if (m.matches) knopf.remove(); });
|
||
} catch { /* aeltere Browser koennen das nicht -- kein Grund zu scheitern */ }
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
WOHIN DER KNOPF GEHOERT
|
||
---------------------------------------------------------------------
|
||
Zwei Faelle, und sie schliessen sich aus:
|
||
|
||
* Es gibt eine Kopfleiste -> kopf.js sagt, wohin (es kennt die
|
||
Reihenfolge der Knoepfe darin).
|
||
* Es gibt keine -> dann steht auf der Seite ein Platz mit
|
||
`data-install-platz`, und wir setzen uns selbst hinein.
|
||
|
||
Der zweite Fall wird NICHT geraten: Ohne diesen Platz passiert
|
||
nichts. Ein Knopf, der sich irgendwohin haengt, weil er sonst
|
||
nirgends hinpasst, landet frueher oder spaeter mitten im Text.
|
||
--------------------------------------------------------------------- */
|
||
window.installKnopfBauen = knopfBauen;
|
||
|
||
const platz = document.querySelector('[data-install-platz]');
|
||
if (platz) knopfBauen(platz);
|
||
})();
|