Comment le contenu interactif de la BBC fonctionne sur AMP, les applications et le Web
Publié: 2022-03-10Dans l'équipe Visual Journalism de la BBC, nous produisons un contenu visuel passionnant, engageant et interactif, allant des calculatrices aux visualisations de nouveaux formats de narration.
Chaque application est un défi unique à produire en soi, mais encore plus si l'on considère que nous devons déployer la plupart des projets dans de nombreuses langues différentes. Notre contenu doit fonctionner non seulement sur les sites Web BBC News and Sports, mais sur leurs applications équivalentes sur iOS et Android, ainsi que sur des sites tiers qui consomment du contenu BBC.
Considérez maintenant qu'il existe un nombre croissant de nouvelles plates-formes telles que AMP, Facebook Instant Articles et Apple News. Chaque plate-forme a ses propres limites et son propre mécanisme de publication. Créer du contenu interactif qui fonctionne dans tous ces environnements est un véritable défi. Je vais décrire comment nous avons abordé le problème à la BBC.
Exemple : Canonical contre AMP
Tout cela est un peu théorique jusqu'à ce que vous le voyiez en action, alors plongeons directement dans un exemple.
Voici un article de la BBC contenant du contenu de journalisme visuel :

Il s'agit de la version canonique de l'article, c'est-à-dire la version par défaut, que vous obtiendrez si vous accédez à l'article depuis la page d'accueil.
Regardons maintenant la version AMP de l'article :

Bien que les versions canonique et AMP se ressemblent, il s'agit en fait de deux points de terminaison différents avec un comportement différent :
- La version canonique vous fait défiler jusqu'au pays de votre choix lorsque vous soumettez le formulaire.
- La version AMP ne vous fait pas défiler, car vous ne pouvez pas faire défiler la page parent à partir d'un iframe AMP.
- La version AMP affiche une iframe recadrée avec un bouton "Afficher plus", en fonction de la taille de la fenêtre et de la position de défilement. C'est une fonctionnalité d'AMP.
En plus des versions canoniques et AMP de cet article, ce projet a également été envoyé à l'application News, qui est encore une autre plate-forme avec ses propres complexités et limites. Alors, comment prenons-nous en charge toutes ces plates-formes ?
L'outillage est la clé
Nous ne construisons pas notre contenu à partir de zéro. Nous avons un échafaudage basé sur Yeoman qui utilise Node pour générer un projet passe-partout avec une seule commande.
Les nouveaux projets sont livrés avec Webpack, SASS, le déploiement et une structure de composants prêts à l'emploi. L'internationalisation est également intégrée à nos projets, à l'aide d'un système de modèles Handlebars. Tom Maslen écrit à ce sujet en détail dans son article, 13 conseils pour rendre la conception Web réactive multilingue.
Prêt à l'emploi, cela fonctionne plutôt bien pour compiler pour une plate-forme, mais nous devons prendre en charge plusieurs plates-formes . Plongeons dans un peu de code.
Intégré ou autonome
Dans le journalisme visuel, nous publions parfois notre contenu dans un iframe afin qu'il puisse être une « intégration » autonome dans un article, non affecté par le script et le style globaux. Un exemple de ceci est l'interactif Donald Trump intégré dans l'exemple canonique plus haut dans cet article.
D'un autre côté, nous produisons parfois notre contenu au format HTML brut. Nous ne le faisons que lorsque nous contrôlons toute la page ou si nous avons besoin d'une interaction de défilement vraiment réactive. Appelons-les respectivement nos sorties "intégrées" et "autonomes".
Imaginons comment nous pourrions construire le "Est-ce qu'un robot prendra votre travail?" interactif dans les formats « intégré » et « autonome ».

Les deux versions du contenu partageraient la grande majorité de leur code, mais il y aurait des différences cruciales dans l'implémentation du JavaScript entre les deux versions.
Par exemple, regardez le bouton "Découvrir mon risque d'automatisation". Lorsque l'utilisateur appuie sur le bouton d'envoi, il doit automatiquement défiler jusqu'à ses résultats.
La version "autonome" du code pourrait ressembler à ceci :
button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });
Mais si vous construisiez ceci en tant que sortie "intégrée", vous savez que votre contenu est à l'intérieur d'un iframe, vous devrez donc le coder différemment :
// 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); } });
De plus, que se passe-t-il si notre application doit passer en plein écran ? C'est assez simple si vous êtes dans une page "autonome":
document.body.className += ' fullscreen';
.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }

Si nous essayions de le faire depuis un "embed", ce même code aurait le contenu mis à l'échelle à la largeur et à la hauteur de l' iframe plutôt qu'à la fenêtre :

…donc en plus d'appliquer le style plein écran à l'intérieur de l'iframe, nous devons envoyer un message à la page hôte pour appliquer le style à l'iframe lui-même :
// iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });
Cela peut se traduire par beaucoup de code spaghetti lorsque vous commencez à prendre en charge plusieurs plates-formes :
button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });
Imaginez faire un équivalent pour chaque interaction DOM significative dans votre projet. Une fois que vous avez fini de frissonner, préparez-vous une tasse de thé relaxante et lisez la suite.
L'abstraction est la clé
Plutôt que de forcer nos développeurs à gérer ces conditions dans leur code, nous avons construit une couche d'abstraction entre leur contenu et l'environnement. Nous appelons cette couche le "wrapper".
Au lieu d'interroger directement le DOM ou les événements du navigateur natif, nous pouvons désormais proxy notre requête via le module wrapper
.
import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });
Chaque plate-forme a sa propre implémentation d'encapsuleur conforme à une interface commune de méthodes d'encapsuleur. Le wrapper s'enroule autour de notre contenu et gère la complexité pour nous.

L'implémentation de la fonction scrollTo
par le wrapper autonome est très simple, passant notre argument directement à window.scrollTo
sous le capot.
Examinons maintenant un wrapper séparé implémentant la même fonctionnalité pour l'iframe :

Le wrapper "embed" prend le même argument que dans l'exemple "standalone" mais manipule la valeur de sorte que le décalage de l'iframe soit pris en compte. Sans cet ajout, nous aurions fait défiler notre utilisateur quelque part de manière totalement involontaire.
Le motif d'emballage
L'utilisation de wrappers donne un code plus propre, plus lisible et cohérent entre les projets. Cela permet également des micro-optimisations au fil du temps, car nous apportons des améliorations progressives aux wrappers pour rendre leurs méthodes plus performantes et accessibles. Votre projet peut donc bénéficier de l'expérience de nombreux développeurs.
Alors, à quoi ressemble un wrapper ?
Structure d'emballage
Chaque wrapper comprend essentiellement trois éléments : un modèle Handlebars, un fichier JS de wrapper et un fichier SASS indiquant le style spécifique au wrapper. De plus, il existe des tâches de construction qui s'accrochent aux événements exposés par l'échafaudage sous-jacent afin que chaque wrapper soit responsable de sa propre pré-compilation et de son propre nettoyage.
Voici une vue simplifiée de l'encapsuleur d'intégration :
embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss
Notre échafaudage sous-jacent expose votre modèle de projet principal en tant que partiel Handlebars, qui est consommé par le wrapper. Par exemple, templates/wrapper.hbs
peut contenir :
<div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>
scss/wrapper.scss
contient un style spécifique au wrapper que votre code d'application ne devrait pas avoir besoin de définir lui-même. L'encapsuleur d'intégration, par exemple, reproduit une grande partie du style de BBC News à l'intérieur de l'iframe.
Enfin, js/wrapper.js
contient l'implémentation iframe de l'API wrapper, détaillée ci-dessous. Il est livré séparément au projet, plutôt que compilé avec le code de l'application - nous marquons wrapper
comme global dans notre processus de construction Webpack. Cela signifie que même si nous livrons notre application sur plusieurs plates-formes, nous ne compilons le code qu'une seule fois.
API d'encapsulation
L'API wrapper résume un certain nombre d'interactions clés avec le navigateur. Voici les plus importants :
scrollTo(int)
Fait défiler jusqu'à la position donnée dans la fenêtre active. Le wrapper normalisera l'entier fourni avant de déclencher le défilement afin que la page hôte défile jusqu'à la position correcte.

getScrollPosition: int
Renvoie la position de défilement actuelle (normalisée) de l'utilisateur. Dans le cas de l'iframe, cela signifie que la position de défilement transmise à votre application est en fait négative jusqu'à ce que l'iframe soit en haut de la fenêtre d'affichage. C'est super utile et nous permet de faire des choses comme animer un composant uniquement lorsqu'il apparaît.
onScroll(callback)
Fournit un crochet dans l'événement de défilement. Dans le wrapper autonome, il s'agit essentiellement de se connecter à l'événement de défilement natif. Dans le wrapper d'intégration, il y aura un léger retard dans la réception de l'événement de défilement puisqu'il est passé via postMessage.
viewport: {height: int, width: int}
Une méthode pour récupérer la hauteur et la largeur de la fenêtre (puisque cela est implémenté très différemment lorsqu'il est interrogé à partir d'un iframe).
toggleFullScreen
En mode autonome, nous masquons le menu et le pied de page de la BBC et définissons une position: fixed
sur notre contenu. Dans l'application News, nous ne faisons rien du tout - le contenu est déjà en plein écran. Le plus compliqué est l'iframe, qui repose sur l'application de styles à l'intérieur et à l'extérieur de l'iframe, coordonné via postMessage.
markPageAsLoaded
Dites au wrapper que votre contenu a été chargé. Ceci est crucial pour que notre contenu fonctionne dans l'application News, qui n'essaiera pas d'afficher notre contenu à l'utilisateur tant que nous n'aurons pas explicitement indiqué à l'application que notre contenu est prêt. Il supprime également le spinner de chargement sur les versions Web de notre contenu.
Liste des emballages
À l'avenir, nous envisageons de créer des wrappers supplémentaires pour les grandes plateformes telles que Facebook Instant Articles et Apple News. Nous avons créé six wrappers à ce jour :
Emballage autonome
La version de notre contenu qui devrait aller dans des pages autonomes. Livré avec la marque BBC.
Intégrer l'emballage
La version iframe de notre contenu, qui peut être insérée en toute sécurité dans des articles ou syndiquée sur des sites non BBC, puisque nous gardons le contrôle du contenu.
Enveloppe AMP
Il s'agit du point de terminaison qui est inséré en tant amp-iframe
dans les pages AMP.
Emballage d'application d'actualités
Notre contenu doit faire des appels à un protocole propriétaire bbcvisualjournalism://
.
Emballage de base
Contient uniquement le HTML - aucun des CSS ou JavaScript de notre projet.
Emballage JSON
Une représentation JSON de notre contenu, pour le partage entre les produits de la BBC.
Câblage Wrappers jusqu'aux plates-formes
Pour que notre contenu apparaisse sur le site de la BBC, nous fournissons aux journalistes un chemin avec espace de noms :
/include/[department]/[unique ID], eg
/include/visual-journalism/123-quiz
Le journaliste met ce « chemin d'inclusion » dans le CMS, qui enregistre la structure de l'article dans la base de données. Tous les produits et services se trouvent en aval de ce mécanisme de publication. Chaque plate-forme est responsable de choisir la saveur du contenu qu'elle souhaite et de demander ce contenu à un serveur proxy.
Prenons ce Donald Trump interactif de tout à l'heure. Ici, le chemin d'inclusion dans le CMS est :
/include/newsspec/15996-trump-tracker/english/index
La page d'article canonique sait qu'elle veut la version "intégrée" du contenu, elle ajoute donc /embed
au chemin d'inclusion :
/include/newsspec/15996-trump-tracker/english/index
/embed
… avant de le demander au serveur proxy :
https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed
La page AMP, d'autre part, voit le chemin d'inclusion et ajoute /amp
:
/include/newsspec/15996-trump-tracker/english/index
/amp
Le rendu AMP fait un peu de magie pour restituer du HTML AMP qui fait référence à notre contenu, en extrayant la version /amp
en tant qu'iframe :
<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>
Chaque plate-forme prise en charge a sa propre version du contenu :
/include/newsspec/15996-trump-tracker/english/index
/amp
/include/newsspec/15996-trump-tracker/english/index
/core
/include/newsspec/15996-trump-tracker/english/index
/envelope
...etc
Cette solution peut évoluer pour intégrer davantage de types de plates-formes au fur et à mesure de leur apparition.
L'abstraction est difficile
Construire une architecture "écrire une fois, déployer n'importe où" semble assez idéaliste, et c'est le cas. Pour que l'architecture wrapper fonctionne, nous devons être très stricts sur le travail dans l'abstraction. Cela signifie que nous devons lutter contre la tentation de "faire ce truc hacky pour que cela fonctionne dans [insérer le nom de la plate-forme ici]". Nous voulons que notre contenu soit complètement inconscient de l'environnement dans lequel il est expédié, mais c'est plus facile à dire qu'à faire.
Les fonctionnalités de la plate-forme sont difficiles à configurer de manière abstraite
Avant notre approche d'abstraction, nous avions un contrôle total sur chaque aspect de notre sortie, y compris, par exemple, le balisage de notre iframe. Si nous devions modifier quoi que ce soit par projet, comme ajouter un attribut de title
à l'iframe pour des raisons d'accessibilité, nous pourrions simplement modifier le balisage.
Maintenant que le balisage wrapper existe indépendamment du projet, la seule façon de le configurer serait d'exposer un crochet dans l'échafaudage lui-même. Nous pouvons le faire relativement facilement pour les fonctionnalités multiplateformes, mais exposer des crochets pour des plates-formes spécifiques rompt l'abstraction. Nous ne voulons pas vraiment exposer une option de configuration 'iframe title' qui n'est utilisée que par un wrapper.
Nous pourrions nommer la propriété de manière plus générique, par exemple title
, puis utiliser cette valeur comme attribut title
de l'iframe. Cependant, il commence à devenir difficile de garder une trace de ce qui est utilisé où, et nous risquons d'abstraire notre configuration au point de ne plus la comprendre. Dans l'ensemble, nous essayons de garder notre configuration aussi simple que possible, en ne définissant que les propriétés qui ont une utilisation globale.
Le comportement des composants peut être complexe
Sur le Web, notre module sharetools crache des boutons de partage de réseaux sociaux qui sont cliquables individuellement et ouvrent un message de partage pré-rempli dans une nouvelle fenêtre.

Dans l'application News, nous ne voulons pas partager via le Web mobile. Si l'utilisateur a installé l'application correspondante (par exemple Twitter), nous souhaitons partager l'application elle-même. Idéalement, nous souhaitons présenter à l'utilisateur le menu de partage iOS/Android natif, puis le laisser choisir son option de partage avant d'ouvrir l'application pour lui avec un message de partage pré-rempli. Nous pouvons déclencher le menu de partage natif depuis l'application en faisant un appel au protocole propriétaire bbcvisualjournalism://
.

Cependant, cet écran se déclenchera si vous appuyez sur « Twitter » ou « Facebook » dans la section « Partagez vos résultats », de sorte que l'utilisateur finit par devoir faire son choix deux fois ; la première fois dans notre contenu, et une seconde fois sur la popup native.
Il s'agit d'un parcours utilisateur étrange, nous souhaitons donc supprimer les icônes de partage individuelles de l'application News et afficher un bouton de partage générique à la place. Nous pouvons le faire en vérifiant explicitement quel wrapper est utilisé avant de rendre le composant.

La construction de la couche d'abstraction wrapper fonctionne bien pour les projets dans leur ensemble, mais lorsque votre choix de wrapper affecte les changements au niveau du composant , il est très difficile de conserver une abstraction propre. Dans ce cas, nous avons perdu un peu d'abstraction et nous avons une logique de bifurcation désordonnée dans notre code. Heureusement, ces cas sont rares et espacés.
Comment gérons-nous les fonctionnalités manquantes ?
Garder l'abstraction, c'est bien beau. Notre code indique au wrapper ce qu'il veut que la plate-forme fasse, par exemple "passer en plein écran". Mais que se passe-t-il si la plate-forme sur laquelle nous expédions ne peut pas réellement passer en plein écran ?
L'emballage fera de son mieux pour ne pas se casser complètement, mais en fin de compte, vous avez besoin d'un design qui revient gracieusement à une solution de travail, que la méthode réussisse ou non. Nous devons concevoir défensivement.
Disons que nous avons une section de résultats contenant des graphiques à barres. Nous aimons souvent garder les valeurs des graphiques à barres à zéro jusqu'à ce que les graphiques défilent dans la vue, moment auquel nous déclenchons l'animation des barres à leur largeur correcte.

Mais si nous n'avons aucun mécanisme pour accrocher la position de défilement - comme c'est le cas dans notre wrapper AMP - alors les barres resteraient à jamais à zéro, ce qui est une expérience complètement trompeuse.

Nous essayons de plus en plus d'adopter une approche d'amélioration progressive dans nos conceptions. Par exemple, nous pourrions fournir un bouton qui sera visible pour toutes les plates-formes par défaut, mais qui sera masqué si le wrapper prend en charge le défilement. De cette façon, si le défilement ne déclenche pas l'animation, l'utilisateur peut toujours déclencher l'animation manuellement.

Projets pour le futur
Nous espérons développer de nouveaux emballages pour des plates-formes telles que Apple News et Facebook Instant Articles, ainsi que pour offrir à toutes les nouvelles plates-formes une version "de base" de notre contenu prête à l'emploi.
Nous espérons également nous améliorer en amélioration progressive; réussir dans ce domaine, c'est se développer défensivement. Vous ne pouvez jamais supposer que toutes les plates-formes actuelles et futures prendront en charge une interaction donnée, mais un projet bien conçu devrait être en mesure de faire passer son message principal sans tomber au premier obstacle technique.
Travailler dans les limites de l'emballage est un peu un changement de paradigme et ressemble un peu à une solution à mi-chemin en termes de solution à long terme . Mais jusqu'à ce que l'industrie mûrisse sur une norme multiplateforme, les éditeurs seront obligés de déployer leurs propres solutions, ou d'utiliser des outils tels que Distro pour la conversion de plateforme à plateforme, ou bien d'ignorer complètement des pans entiers de leur public.
Nous n'en sommes qu'à nos débuts, mais jusqu'à présent, nous avons eu beaucoup de succès en utilisant le modèle wrapper pour créer notre contenu une fois et le diffuser sur la myriade de plates-formes que nos publics utilisent maintenant.