Abo-Lotse: Making-of einer Browsererweiterung

Abo-Lotse: Making-of einer Browsererweiterung
Photo by Ioana Han / Unsplash

Ein Abo, vier Zeitungen und eine Bezahlschranke

Ich habe ein Digitalabo der Basler Zeitung. Trotzdem lande ich ständig vor einer Bezahlschranke, und zwar bei Artikeln, die ich eigentlich längst bezahlt habe. Tamedia veröffentlicht nämlich denselben Artikel auf mehreren Titeln gleichzeitig, doch mein Abo gilt nur auf bazonline.ch. Schickt mir jemand einen Link auf tagesanzeiger.ch oder lande ich über Google News bei der Berner Zeitung, bin ich dort für die Website ein Gast ohne Abo. Statt des Artikels sehe ich ein paar Zeilen Vorschau und die Aufforderung, ein weiteres Abo abzuschliessen. Dabei steht exakt derselbe Text auf bazonline.ch und wäre für mich mit einem Klick lesbar, wenn ich nur wüsste, dass es ihn dort gibt.

Irgendwann hatte ich genug davon, die URL von Hand umzuschreiben. Also habe ich eine Browsererweiterung gebaut, die genau das für mich erledigt. Sie heisst Abo-Lotse, läuft unter Firefox und Chrome und ist inzwischen in beiden Stores verfügbar. In diesem Artikel erzähle ich dir, wie sie entstanden ist, welche Stolpersteine es gab und was ich bei den Reviews von Mozilla und Google gelernt habe.

Die Idee: ein Lotse, kein Dietrich

Das Prinzip ist schnell erklärt. Du wählst nach der Installation einmal deine eigene Zeitung, bei mir die Basler Zeitung. Öffnest du danach einen Artikel bei einem Schwestertitel, prüft Abo-Lotse im Hintergrund, ob derselbe Artikel auch bei deiner Zeitung erscheint. Falls ja, zeigt die Erweiterung einen kurzen Hinweis mit Countdown und leitet dich dann weiter. Falls nein, bleibst du einfach auf der Seite.

Mir war von Anfang an wichtig, was Abo-Lotse nicht tut. Die Erweiterung schaltet nichts frei und umgeht keine Bezahlschranke. Sie führt dich nur dorthin, wo du mit deinem eigenen Abo ohnehin lesen darfst. Deshalb leitet sie nie über Verlagsgrenzen hinweg weiter, nie von deiner eigenen Zeitung weg und nie, wenn die Prüfung fehlschlägt. Im Zweifel passiert lieber gar nichts.

In Version 1.0 unterstützt Abo-Lotse die Tamedia Titel in der Deutschschweiz (Tages-Anzeiger, Basler Zeitung, Berner Zeitung, Der Bund) und in der Romandie (24 heures, Tribune de Genève). CH Media fehlt noch, ganz einfach weil ich dort kein Abo habe und die Erweiterung nicht ohne echten Test veröffentlichen wollte.

Einstellungen von Abo-Lotse

Eine Codebasis für zwei Browser

Ich wollte Firefox und Chrome mit demselben Code bedienen. Früher hiess das: Polyfills, zwei Manifeste und viel Handarbeit. Heute nehme ich dafür WXT, ein Framework für Browsererweiterungen auf Basis von Vite. Es baut aus einer Konfiguration die Pakete für beide Browser, bringt Live Reload mit und erzeugt sogar das Quellcodearchiv, das Mozilla beim Review verlangt.

Geschrieben ist alles in TypeScript, und zwar ohne eine einzige Laufzeitabhängigkeit. Was im Store landet, ist ausschliesslich mein eigener Code. Bei einer Erweiterung, die auf Zeitungsseiten mitliest, finde ich das ein wichtiges Argument.

Interessant war, dass WXT für Firefox standardmässig noch Manifest V2 baut. Ich habe bewusst für beide Browser auf Manifest V3 gesetzt, damit sich der Code nicht auseinanderentwickelt. Die Unterschiede, die bleiben, sind erstaunlich klein. Firefox will beim Autor einen einfachen Namen, Chrome ein Objekt mit E-Mail Adresse. Firefox braucht zusätzlich eine feste Erweiterungs ID und die neue Angabe zur Datenerhebung. Mehr Weichen gibt es in der ganzen Konfiguration nicht.

Die Architektur ist bewusst schlicht gehalten. Die gesamte Logik für Erkennung, URL Aufbau und Weiterleitungsentscheid steckt in reinen Funktionen ohne Browser APIs. Das macht sie mit Vitest testbar, ohne einen Browser zu starten. Ein Content Script erkennt Artikelseiten, ein Hintergrundskript übernimmt die Prüfung, und Popup sowie Einstellungsseite zeigen den Status. Welche Zeitungen unterstützt werden, steht in einer einzigen Datei. Daraus generiert der Build auch die Berechtigungen im Manifest, sodass ich keine Domain an zwei Orten pflegen muss.

Das eigentliche Problem: Gibt es den Artikel überhaupt?

Die Weiterleitung selbst ist trivial. Tamedia Artikel enden auf eine zwölfstellige ID, die für alle Titel einer Sprachregion gleich ist. Abo-Lotse muss also nur den Hostnamen austauschen und Pfad, Parameter sowie Anker behalten. Die Erkennung ist ein einziger regulärer Ausdruck:

const TAMEDIA_ARTICLE_ID = /-(\d{12})\/?$/;

Nur erscheint nicht jeder Artikel auf jedem Titel. Einen Lokalbericht aus Bern gibt es bei der Basler Zeitung schlicht nicht. Blind weiterzuleiten wäre deshalb keine Option gewesen, denn dann würdest du regelmässig auf einer Fehlerseite landen. Abo-Lotse fragt deshalb vorher nach.

Dafür schickt das Hintergrundskript zuerst einen HEAD Request an die Ziel URL. Ein 404 bedeutet klar, dass der Artikel fehlt. Ein 200 allein reicht aber nicht, denn Verlagsseiten leiten fehlende Artikel gerne auf eine Rubrikenseite um. Ich vergleiche deshalb die endgültige URL nach allen Weiterleitungen: Nur wenn der Host stimmt und darin dieselbe Artikel ID steckt, gilt der Artikel als vorhanden. Unterstützt ein Server kein HEAD, fällt Abo-Lotse auf GET zurück und liest bei Bedarf og:url und canonical aus dem HTML. Weil Service Worker kein DOM haben, passiert das mit einem bewusst einfachen regulären Ausdruck statt mit einem Parser.

Der grösste Stolperstein kam beim ersten Test. Meine Anfragen liefen in eine Endlosschleife aus 302 Weiterleitungen. Tamedia beantwortet eine erste Anfrage ohne Cookies mit einer Weiterleitung auf sich selbst, die ein Cookie setzt. Ohne Cookie dreht sich das ewig im Kreis. Die Lösung war, die Prüfung mit credentials: 'include' zu senden. Das musste ich später auch im Review bei Mozilla begründen.

Damit die Erweiterung keine unnötige Last erzeugt, merkt sie sich Ergebnisse zehn Minuten lang im Sitzungsspeicher. Öffnest du denselben Artikel in zwei Tabs, teilen sich beide eine einzige Anfrage. Nach drei Sekunden ohne Antwort bricht die Prüfung ab, und ein unklares Ergebnis führt nie zu einer Weiterleitung.

Ein Hinweis, der nicht nervt

Eine Erweiterung, die dich ungefragt auf eine andere Seite schiebt, wirkt schnell übergriffig. Deshalb zeigt Abo-Lotse vor der Weiterleitung einen kurzen Hinweis mit Countdown und zwei Knöpfen: «Jetzt lesen» und «Hier bleiben». Den Countdown kannst du zwischen null und fünf Sekunden einstellen. Bei null wird sofort weitergeleitet, ohne dass der Hinweis überhaupt erscheint. Entscheidest du dich fürs Bleiben, merkt sich Abo-Lotse das für diesen Artikel, bis du den Browser schliesst.

Weiterleitungs Hinweis von Abo-Lotse

Gibt es den Artikel bei deiner Zeitung nicht, erscheint ein kleiner, nicht modaler Hinweis. Er kommt pro Artikel und Sitzung nur einmal und lässt sich ganz abschalten. Zusätzlich zeigt das Symbol in der Werkzeugleiste ein graues Zeichen, damit du den Status auch ohne Hinweis siehst.

Der Hinweis lebt in einem Shadow DOM, damit sich die Stile der Zeitungsseite und meine nicht in die Quere kommen. Ganz gereicht hat das trotzdem nicht. Einige Seiten setzen Schriften so global zurück, dass meine Typografie zerfiel, und auf vollgepackten Newsseiten verschwand der Hinweis anfangs hinter anderen Elementen. Beides brauchte eigene Korrekturen.

Weil ich beruflich viel mit Barrierefreiheit zu tun habe, war das für mich kein Zusatz, sondern Pflicht. Der Dialog ist sauber ausgezeichnet, fängt den Fokus ein und gibt ihn danach zurück, lässt sich vollständig per Tastatur bedienen und funktioniert mit Screenreadern. Die Fortschrittsanimation respektiert die Einstellung für reduzierte Bewegung, und der Dunkelmodus wird automatisch übernommen. Die Oberfläche gibt es auf Deutsch, Französisch, Italienisch und Englisch, passend für eine Schweizer Erweiterung.

Datenschutz: so wenig wie möglich

Abo-Lotse hat keine Telemetrie, keine Analyse und keinen eigenen Server. Die einzigen Netzwerkanfragen gehen an die Websites der unterstützten Zeitungen, und zwar genau eine pro Artikel an deine eigene Zeitung. Deine Einstellungen liegen im Erweiterungsspeicher des Browsers. Prüfergebnisse und deine «Hier bleiben» Entscheide verschwinden mit dem Ende der Browsersitzung.

Auch bei den Berechtigungen bin ich sparsam geblieben. Abo-Lotse verlangt nur storage und Zugriff auf die sechs Zeitungsdomains. Keine Tabs, keine History, kein Zugriff auf alle Websites. Weil die Domains direkt aus der Konfiguration erzeugt werden, kann sich da auch nicht versehentlich mehr einschleichen.

Die Datenschutzerklärung liegt in vier Sprachen im Repository. Bei einer Erweiterung ohne Datenerhebung ist sie kurz, aber beide Stores verlangen sie trotzdem.

Zwei Stores, zwei Philosophien

Der Code war schneller fertig als die Veröffentlichung. Beide Stores wollen Beschreibungen, Screenshots, Begründungen für jede Berechtigung und eine Datenschutzerklärung. Ich habe alle Texte in vier Sprachen geschrieben und für die Screenshots eine neutrale Platzhalterseite ohne Verlagslogos gebaut. Die Erweiterung darauf ist echt, die Zeitung nicht. Logos und Farben der Verlage wollte ich bewusst nicht verwenden, und auch das Wort «Paywall» taucht nirgends prominent auf. Abo-Lotse soll nicht wie ein Werkzeug zum Umgehen wirken, weil es keines ist.

Bei Mozilla fällt auf, wie ernst die Nachvollziehbarkeit genommen wird. Weil der Build aus TypeScript erzeugt wird, verlangt addons.mozilla.org den Quellcode und eine Anleitung, mit der die Prüfer exakt dasselbe Paket reproduzieren können. Dank fixierter pnpm Version und Lockfile ist das ein Dreizeiler. Dazu kommt die neue Pflichtangabe zur Datenerhebung im Manifest, bei mir schlicht «none». In den Notizen für die Prüfer habe ich erklärt, warum die Erweiterung Anfragen mit Cookies sendet. Ohne diese Erklärung hätte das vermutlich Fragen ausgelöst.

Google prüft anders. Hier geht es weniger um den Code als um das Store Listing. Meine erste Beschreibung enthielt eine Liste der unterstützten Zeitungen, was für mich die naheliegendste Information überhaupt war. Google lehnte die Einreichung trotzdem ab, mit der Begründung Keyword Spam. Eine Aufzählung von Markennamen wertet der Chrome Web Store offenbar als Versuch, die Suche im Store zu manipulieren. Kurios ist die Form der Ablehnung: Google gibt jeder Art von Richtlinienverstoss einen Codenamen aus einer Farbe und einem chemischen Element. Keyword Spam heisst dort «Yellow Argon», und genau dieser Begriff stand in meiner Ablehnungsmail. Die Lösung war einfach: Die Liste flog aus der Beschreibung, stattdessen verweise ich auf die Willkommensseite und das GitHub Repository. Danach ging es durch.

Damit ich bei künftigen Versionen nichts von Hand zusammenklicken muss, baut GitHub Actions bei jedem Tag automatisch beide Pakete samt Quellcodearchiv. Vorher laufen Linting, Typprüfung, Tests und web-ext lint, und der Workflow bricht ab, wenn Tag und Versionsnummer nicht übereinstimmen.

Fazit und wie es weitergeht

Abo-Lotse löst ein kleines, sehr schweizerisches Problem. Gerade deshalb hat mir das Projekt Spass gemacht: Die eigentliche Logik passt in wenige hundert Zeilen, die Sorgfalt steckt in den Randfällen. Wann darf weitergeleitet werden, wann auf keinen Fall, und wie bleibt das Ganze respektvoll gegenüber dir und gegenüber den Verlagen? Mit WXT war die Arbeit an zwei Browsern dabei fast schon langweilig einfach. Die meiste Zeit habe ich am Ende nicht mit Code verbracht, sondern mit Store Texten, Screenshots und Reviews.

Als Nächstes würde ich gerne CH Media unterstützen. Dafür brauche ich aber jemanden mit Abo, der testet, wie die Artikel URLs dort aufgebaut sind und wie die Server auf fehlende Artikel reagieren. Ein neuer Verlag ist im Code nur ein Eintrag in einer Konfigurationsdatei. Wenn du helfen möchtest oder einen Fehler findest, eröffne gerne ein Issue im GitHub Repository.

Abo-Lotse steht unter der GPLv2 und ist kostenlos erhältlich für Firefox und Chrome.