Wie interaktive BBC-Inhalte in AMP, Apps und im Web funktionieren
Veröffentlicht: 2022-03-10Im Team für visuellen Journalismus bei der BBC produzieren wir spannende visuelle, ansprechende und interaktive Inhalte, die von Taschenrechnern bis hin zu Visualisierungen und neuen Storytelling-Formaten reichen.
Jede Anwendung ist für sich genommen eine einzigartige Herausforderung, aber umso mehr, wenn man bedenkt, dass wir die meisten Projekte in vielen verschiedenen Sprachen bereitstellen müssen. Unsere Inhalte müssen nicht nur auf den BBC-Nachrichten- und Sport-Websites funktionieren, sondern auch auf den entsprechenden Apps auf iOS und Android sowie auf Websites von Drittanbietern, die BBC-Inhalte nutzen.
Bedenken Sie nun, dass es immer mehr neue Plattformen wie AMP, Facebook Instant Articles und Apple News gibt. Jede Plattform hat ihre eigenen Einschränkungen und proprietären Veröffentlichungsmechanismen. Das Erstellen interaktiver Inhalte, die in all diesen Umgebungen funktionieren, ist eine echte Herausforderung. Ich werde beschreiben, wie wir das Problem bei der BBC angegangen sind.
Beispiel: Canonical vs. AMP
Das ist alles ein bisschen theoretisch, bis Sie es in Aktion sehen, also lassen Sie uns direkt auf ein Beispiel eingehen.
Hier ist ein BBC-Artikel mit Inhalten zu Visual Journalism:

Dies ist die kanonische Version des Artikels, dh die Standardversion, die Sie erhalten, wenn Sie von der Homepage zu dem Artikel navigieren.
Schauen wir uns nun die AMP-Version des Artikels an:

Während die kanonische und die AMP-Version gleich aussehen, handelt es sich tatsächlich um zwei verschiedene Endpunkte mit unterschiedlichem Verhalten:
- Die kanonische Version scrollt Sie zu Ihrem ausgewählten Land, wenn Sie das Formular absenden.
- Die AMP-Version scrollt Sie nicht, da Sie die übergeordnete Seite nicht innerhalb eines AMP-Iframes scrollen können.
- Die AMP-Version zeigt je nach Größe des Darstellungsbereichs und Bildlaufposition einen beschnittenen Iframe mit der Schaltfläche „Mehr anzeigen“. Dies ist eine Funktion von AMP.
Neben den kanonischen und AMP-Versionen dieses Artikels wurde dieses Projekt auch an die News-App gesendet, die eine weitere Plattform mit eigenen Feinheiten und Einschränkungen ist. Wie unterstützen wir also all diese Plattformen?
Werkzeuge sind der Schlüssel
Wir erstellen unsere Inhalte nicht von Grund auf neu. Wir haben ein Yeoman-basiertes Gerüst, das Node verwendet, um ein Boilerplate-Projekt mit einem einzigen Befehl zu generieren.
Neue Projekte werden standardmäßig mit Webpack, SASS, Deployment und einer Komponentenstruktur geliefert. Die Internationalisierung wird auch in unsere Projekte integriert, indem wir ein Handlebars-Template-System verwenden. Darüber schreibt Tom Maslen ausführlich in seinem Beitrag 13 Tipps, wie man responsives Webdesign mehrsprachig macht.
Standardmäßig funktioniert dies ziemlich gut für die Kompilierung für eine Plattform, aber wir müssen mehrere Plattformen unterstützen . Lassen Sie uns in etwas Code eintauchen.
Einbetten vs. Standalone
Im visuellen Journalismus geben wir unsere Inhalte manchmal in einem Iframe aus, damit sie in sich geschlossen in einen Artikel „eingebettet“ werden können, unbeeinflusst von der globalen Skripterstellung und Gestaltung. Ein Beispiel dafür ist das interaktive Donald Trump, das in das kanonische Beispiel weiter oben in diesem Artikel eingebettet ist.
Andererseits geben wir unsere Inhalte manchmal als reines HTML aus. Wir tun dies nur, wenn wir die Kontrolle über die gesamte Seite haben oder wenn wir eine wirklich reaktionsschnelle Scroll-Interaktion benötigen. Nennen wir diese unsere „eingebetteten“ bzw. „eigenständigen“ Ausgänge.
Stellen wir uns vor, wie wir das „Wird ein Roboter Ihren Job übernehmen?“ bauen könnten. interaktiv sowohl im „eingebetteten“ als auch im „eigenständigen“ Format.

Beide Versionen des Inhalts würden den Großteil ihres Codes teilen, aber es gäbe einige entscheidende Unterschiede in der Implementierung des JavaScripts zwischen den beiden Versionen.
Sehen Sie sich zum Beispiel die Schaltfläche „Finde mein Automatisierungsrisiko“ an. Wenn der Benutzer auf die Schaltfläche „Senden“ klickt, sollte er automatisch zu seinen Ergebnissen gescrollt werden.
Die „eigenständige“ Version des Codes könnte so aussehen:
button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });
Aber wenn Sie dies als „Einbettungs“-Ausgabe erstellen, wissen Sie, dass sich Ihr Inhalt in einem Iframe befindet, also müssten Sie ihn anders codieren:
// inside the iframe button.on('click', () => { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); }); // inside the host page window.addEventListener('message', (event) => { if (event.data.name === 'scroll') { window.scrollTo(0, iframe.offsetTop + event.data.offset); } });
Was ist auch, wenn unsere Anwendung im Vollbildmodus angezeigt werden muss? Dies ist ganz einfach, wenn Sie sich auf einer „eigenständigen“ Seite befinden:
document.body.className += ' fullscreen';
.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }

Wenn wir versuchten, dies innerhalb einer „Einbettung“ zu tun, würde derselbe Code den Inhalt auf die Breite und Höhe des iframe skalieren und nicht auf den Ansichtsbereich:

… also müssen wir zusätzlich zum Anwenden des Vollbildstils innerhalb des Iframes eine Nachricht an die Hostseite senden, um das Styling auf den Iframe selbst anzuwenden:
// iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });
Dies kann zu einer Menge Spaghetti-Code führen, wenn Sie anfangen, mehrere Plattformen zu unterstützen:
button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });
Stellen Sie sich vor, Sie machen ein Äquivalent davon für jede sinnvolle DOM-Interaktion in Ihrem Projekt. Wenn Sie mit dem Zittern fertig sind, machen Sie sich eine entspannende Tasse Tee und lesen Sie weiter.
Abstraktion ist der Schlüssel
Anstatt unsere Entwickler zu zwingen, diese Bedingungen in ihrem Code zu handhaben, haben wir eine Abstraktionsschicht zwischen ihrem Inhalt und der Umgebung aufgebaut. Wir nennen diese Schicht „Wrapper“.
Anstatt die Ereignisse des DOM oder des nativen Browsers direkt abzufragen, können wir unsere Anfrage jetzt über das wrapper
-Modul weiterleiten.
import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });
Jede Plattform hat ihre eigene Wrapper-Implementierung, die einer gemeinsamen Schnittstelle von Wrapper-Methoden entspricht. Der Wrapper wickelt sich um unseren Inhalt und übernimmt die Komplexität für uns.

Die Implementierung der scrollTo
Funktion durch den Standalone-Wrapper ist sehr einfach und übergibt unser Argument direkt an window.scrollTo
unter der Haube.
Schauen wir uns nun einen separaten Wrapper an, der die gleiche Funktionalität für den Iframe implementiert:

Der „Embed“-Wrapper verwendet dasselbe Argument wie im „Standalone“-Beispiel, manipuliert den Wert jedoch so, dass der Iframe-Offset berücksichtigt wird. Ohne diesen Zusatz hätten wir unseren User völlig unbeabsichtigt irgendwohin gescrollt.
Das Wrapper-Muster
Die Verwendung von Wrappern führt zu Code, der sauberer, besser lesbar und konsistent zwischen Projekten ist. Es ermöglicht auch Mikrooptimierungen im Laufe der Zeit, da wir inkrementelle Verbesserungen an den Wrappern vornehmen, um ihre Methoden leistungsfähiger und zugänglicher zu machen. Ihr Projekt kann somit von der Erfahrung vieler Entwickler profitieren.
Also, wie sieht ein Wrapper aus?
Wrapper-Struktur
Jeder Wrapper besteht im Wesentlichen aus drei Dingen: einer Handlebars-Vorlage, einer Wrapper-JS-Datei und einer SASS-Datei, die das Wrapper-spezifische Styling angibt. Darüber hinaus gibt es Build-Aufgaben, die sich in Ereignisse einklinken, die vom zugrunde liegenden Gerüst offengelegt werden, sodass jeder Wrapper für seine eigene Vorkompilierung und Bereinigung verantwortlich ist.
Dies ist eine vereinfachte Ansicht des Embed-Wrappers:
embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss
Unser zugrunde liegendes Scaffolding macht Ihre Hauptprojektvorlage als Handlebars-Partial verfügbar, das vom Wrapper verwendet wird. Beispielsweise könnte templates/wrapper.hbs
enthalten:
<div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>
scss/wrapper.scss
enthält Wrapper-spezifische Stile, die Ihr Anwendungscode nicht selbst definieren muss. Der Einbettungs-Wrapper repliziert zum Beispiel viel Stil von BBC News innerhalb des Iframes.
Schließlich enthält js/wrapper.js
die Iframe-Implementierung der Wrapper-API, die unten detailliert beschrieben wird. Es wird separat an das Projekt geliefert und nicht mit dem Anwendungscode kompiliert – wir kennzeichnen den wrapper
in unserem Webpack-Erstellungsprozess als global. Das bedeutet, dass wir unsere Anwendung zwar für mehrere Plattformen bereitstellen, aber den Code nur einmal kompilieren.
Wrapper-API
Die Wrapper-API abstrahiert eine Reihe wichtiger Browserinteraktionen. Hier sind die wichtigsten:

scrollTo(int)
Scrollt zur angegebenen Position im aktiven Fenster. Der Wrapper normalisiert die bereitgestellte Ganzzahl, bevor er den Bildlauf auslöst, sodass die Hostseite an die richtige Position gescrollt wird.
getScrollPosition: int
Gibt die aktuelle (normalisierte) Bildlaufposition des Benutzers zurück. Im Fall des Iframe bedeutet dies, dass die an Ihre Anwendung übergebene Bildlaufposition tatsächlich negativ ist, bis sich der Iframe am oberen Rand des Ansichtsfensters befindet. Dies ist sehr nützlich und ermöglicht es uns, Dinge wie das Animieren einer Komponente nur dann zu tun, wenn sie sichtbar ist.
onScroll(callback)
Stellt einen Hook in das Scroll-Ereignis bereit. Im Standalone-Wrapper ist dies im Wesentlichen eine Einbindung in das native Scroll-Ereignis. Im Embed-Wrapper kommt es beim Empfang des Scroll-Ereignisses zu einer leichten Verzögerung, da es über postMessage übergeben wird.
viewport: {height: int, width: int}
Eine Methode zum Abrufen der Höhe und Breite des Ansichtsfensters (da dies sehr unterschiedlich implementiert wird, wenn es innerhalb eines Iframes abgefragt wird).
toggleFullScreen
Im Standalone-Modus blenden wir das BBC-Menü und die Fußzeile aus und setzen eine position: fixed
auf unseren Inhalt. In der News-App machen wir gar nichts – der Inhalt ist bereits bildschirmfüllend. Das Komplizierte ist der Iframe, der auf der Anwendung von Stilen sowohl innerhalb als auch außerhalb des Iframes beruht und über postMessage koordiniert wird.
markPageAsLoaded
Teilen Sie dem Wrapper mit, dass Ihr Inhalt geladen wurde. Dies ist entscheidend, damit unsere Inhalte in der Nachrichten-App funktionieren, die nicht versucht, unsere Inhalte dem Benutzer anzuzeigen, bis wir der App ausdrücklich mitteilen, dass unsere Inhalte bereit sind. Es entfernt auch den Ladedreher in den Webversionen unserer Inhalte.
Liste der Wrapper
In Zukunft planen wir weitere Wrapper für große Plattformen wie Facebook Instant Articles und Apple News zu erstellen. Wir haben bisher sechs Wrapper erstellt:
Eigenständiger Wrapper
Die Version unseres Inhalts, die in eigenständige Seiten eingefügt werden soll. Kommt gebündelt mit BBC-Branding.
Embed-Wrapper
Die Iframe-Version unserer Inhalte, die sicher in Artikeln platziert oder an Nicht-BBC-Sites syndiziert werden können, da wir die Kontrolle über die Inhalte behalten.
AMP-Wrapper
Dies ist der Endpunkt, der als AMP amp-iframe
in AMP-Seiten eingefügt wird.
Nachrichten-App-Wrapper
Unsere Inhalte müssen ein proprietäres bbcvisualjournalism://
Protokoll aufrufen.
Core-Wrapper
Enthält nur den HTML-Code – kein CSS oder JavaScript unseres Projekts.
JSON-Wrapper
Eine JSON-Darstellung unserer Inhalte zum Teilen in BBC-Produkten.
Wiring Wrapper bis zu den Plattformen
Damit unsere Inhalte auf der BBC-Website erscheinen, stellen wir Journalisten einen Namespace-Pfad zur Verfügung:
/include/[department]/[unique ID], eg
/include/visual-journalism/123-quiz
Diesen „Include-Pfad“ gibt der Journalist in das CMS ein, das die Artikelstruktur in der Datenbank speichert. Alle Produkte und Dienstleistungen sind diesem Veröffentlichungsmechanismus nachgelagert. Jede Plattform ist dafür verantwortlich, die gewünschte Art von Inhalt auszuwählen und diesen Inhalt von einem Proxy-Server anzufordern.
Nehmen wir das interaktive Donald Trump von vorhin. Hier lautet der Include-Pfad im CMS:
/include/newsspec/15996-trump-tracker/english/index
Die kanonische Artikelseite weiß, dass sie die „eingebettete“ Version des Inhalts haben möchte, also hängt sie /embed
embed an den Include-Pfad an:
/include/newsspec/15996-trump-tracker/english/index
/embed
…bevor Sie es vom Proxy-Server anfordern:
https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed
Die AMP-Seite hingegen sieht den Include-Pfad und hängt /amp
an:
/include/newsspec/15996-trump-tracker/english/index
/amp
Der AMP-Renderer macht ein wenig Magie, um AMP-HTML zu rendern, das auf unseren Inhalt verweist, indem er die /amp
-Version als Iframe einliest:
<amp-iframe src="https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/amp" width="640" height="360"> <!-- some other AMP elements here --> </amp-iframe>
Jede unterstützte Plattform hat ihre eigene Version des Inhalts:
/include/newsspec/15996-trump-tracker/english/index
/amp
/include/newsspec/15996-trump-tracker/english/index
/core
/include/newsspec/15996-trump-tracker/english/index
/envelope
...und so weiter
Diese Lösung kann skaliert werden, um weitere Plattformtypen zu integrieren, wenn sie entstehen.
Abstraktion ist schwer
Der Aufbau einer „Write Once, Deploy Anywhere“-Architektur klingt ziemlich idealistisch und ist es auch. Damit die Wrapper-Architektur funktioniert, müssen wir sehr strikt innerhalb der Abstraktion arbeiten. Das bedeutet, dass wir der Versuchung widerstehen müssen, „dieses hackige Ding zu machen, damit es in [Plattformnamen hier einfügen] funktioniert“. Wir möchten, dass unsere Inhalte die Umgebung, in der sie versendet werden, überhaupt nicht kennen – aber das ist leichter gesagt als getan.
Funktionen der Plattform lassen sich nur schwer abstrakt konfigurieren
Vor unserem Abstraktionsansatz hatten wir die vollständige Kontrolle über jeden Aspekt unserer Ausgabe, einschließlich beispielsweise des Markups unseres Iframes. Wenn wir etwas auf Projektbasis optimieren mussten, z. B. aus Gründen der Barrierefreiheit ein title
zum Iframe hinzufügen, konnten wir einfach das Markup bearbeiten.
Jetzt, da das Wrapper-Markup isoliert vom Projekt vorhanden ist, besteht die einzige Möglichkeit, es zu konfigurieren, darin, einen Hook im Gerüst selbst verfügbar zu machen. Wir können dies relativ einfach für plattformübergreifende Funktionen tun, aber das Verfügbarmachen von Hooks für bestimmte Plattformen bricht die Abstraktion. Wir möchten nicht wirklich eine Konfigurationsoption „iframe title“ offenlegen, die nur von dem einen Wrapper verwendet wird.
Wir könnten die Eigenschaft allgemeiner benennen, z. B. title
, und diesen Wert dann als iFrame title
verwenden. Es wird jedoch schwierig, den Überblick darüber zu behalten, was wo verwendet wird, und wir riskieren, unsere Konfiguration zu abstrahieren, bis wir sie nicht mehr verstehen. Im Großen und Ganzen versuchen wir, unsere Konfiguration so schlank wie möglich zu halten und nur Eigenschaften zu setzen, die global verwendet werden.
Das Komponentenverhalten kann komplex sein
Im Web spuckt unser Sharetools-Modul Social-Network-Share-Buttons aus, die einzeln anklickbar sind und eine vorab ausgefüllte Share-Nachricht in einem neuen Fenster öffnen.

In der News-App möchten wir nicht über das mobile Web teilen. Wenn der Benutzer die entsprechende Anwendung installiert hat (z. B. Twitter), möchten wir in der App selbst teilen. Idealerweise möchten wir dem Benutzer das native iOS/Android-Freigabemenü präsentieren und ihn dann seine Freigabeoption auswählen lassen, bevor wir die App mit einer vorab ausgefüllten Freigabenachricht für ihn öffnen. Wir können das native Share-Menü von der App aus auslösen, indem wir das proprietäre Protokoll bbcvisualjournalism://
.

Dieser Bildschirm wird jedoch ausgelöst, wenn Sie im Abschnitt „Ergebnisse teilen“ auf „Twitter“ oder „Facebook“ tippen, sodass der Benutzer seine Wahl am Ende zweimal treffen muss; das erste Mal in unserem Inhalt und ein zweites Mal im nativen Popup.
Dies ist eine seltsame Benutzerführung, daher möchten wir die einzelnen Teilen-Symbole aus der News-App entfernen und stattdessen eine generische Teilen-Schaltfläche anzeigen. Wir können dies tun, indem wir explizit prüfen, welcher Wrapper verwendet wird, bevor wir die Komponente rendern.

Das Erstellen der Wrapper-Abstraktionsschicht funktioniert gut für Projekte als Ganzes, aber wenn sich Ihre Wahl des Wrappers auf Änderungen auf Komponentenebene auswirkt, ist es sehr schwierig, eine saubere Abstraktion beizubehalten. In diesem Fall haben wir ein wenig Abstraktion verloren, und wir haben eine unordentliche Forking-Logik in unserem Code. Zum Glück sind diese Fälle selten.
Wie gehen wir mit fehlenden Funktionen um?
Abstraktion zu bewahren ist schön und gut. Unser Code teilt dem Wrapper mit, was die Plattform tun soll, z. B. „Vollbildmodus“. Aber was ist, wenn die Plattform, an die wir versenden, nicht im Vollbildmodus angezeigt werden kann?
Die Hülle wird ihr Bestes geben, um nicht vollständig zu brechen, aber letztendlich brauchen Sie ein Design, das elegant auf eine funktionierende Lösung zurückfällt, unabhängig davon, ob die Methode erfolgreich ist oder nicht. Wir müssen defensiv gestalten.
Nehmen wir an, wir haben einen Ergebnisbereich mit einigen Balkendiagrammen. Wir möchten die Balkendiagrammwerte oft auf Null belassen, bis die Diagramme in die Ansicht gescrollt werden. An diesem Punkt lösen wir die Animation der Balken auf ihre richtige Breite aus.

Aber wenn wir keinen Mechanismus haben, um uns in die Scroll-Position einzuklinken – wie es in unserem AMP-Wrapper der Fall ist – dann würden die Balken für immer auf Null bleiben, was eine durch und durch irreführende Erfahrung ist.

Wir versuchen zunehmend, in unseren Designs einen progressiveren Verbesserungsansatz zu verfolgen. Beispielsweise könnten wir eine Schaltfläche bereitstellen, die standardmäßig für alle Plattformen sichtbar ist, aber ausgeblendet wird, wenn der Wrapper das Scrollen unterstützt. Auf diese Weise kann der Benutzer die Animation immer noch manuell auslösen, wenn der Bildlauf die Animation nicht auslöst.

Pläne für die Zukunft
Wir hoffen, neue Wrapper für Plattformen wie Apple News und Facebook Instant Articles zu entwickeln und allen neuen Plattformen eine „Kern“-Version unserer Inhalte sofort anbieten zu können.
Wir hoffen auch, bei der progressiven Verbesserung besser zu werden; Erfolg in diesem Bereich bedeutet, sich defensiv zu entwickeln. Man kann nie davon ausgehen, dass alle Plattformen jetzt und in Zukunft eine bestimmte Interaktion unterstützen, aber ein gut konzipiertes Projekt sollte in der Lage sein, seine Kernbotschaft zu vermitteln, ohne an der ersten technischen Hürde zu scheitern.
Innerhalb der Grenzen des Wrappers zu arbeiten, ist ein bisschen wie ein Paradigmenwechsel und fühlt sich in Bezug auf die langfristige Lösung wie ein Zwischending an. Aber bis die Branche zu einem plattformübergreifenden Standard heranreift, werden Verlage gezwungen sein, ihre eigenen Lösungen einzuführen oder Tools wie Distro für die Plattform-zu-Plattform-Konvertierung zu verwenden oder ganze Teile ihres Publikums insgesamt zu ignorieren.
Wir stehen noch am Anfang, aber bisher hatten wir großen Erfolg bei der Verwendung des Wrapper-Musters, um unsere Inhalte einmal zu erstellen und sie an die unzähligen Plattformen zu liefern, die unsere Zielgruppen jetzt verwenden.