Come funzionano i contenuti interattivi della BBC su AMP, app e Web

Pubblicato: 2022-03-10
Riepilogo rapido ↬ La pubblicazione di contenuti su così tanti media senza un sovraccarico di sviluppo aggiuntivo può essere difficile. Chris Ashton spiega come hanno affrontato il problema nel dipartimento di Visual Journalism della BBC.

Nel team di Visual Journalism della BBC, produciamo entusiasmanti contenuti visivi, coinvolgenti e interattivi, che vanno dai calcolatori alle visualizzazioni, nuovi formati di narrazione.

Ogni applicazione è una sfida unica da produrre a sé stante, ma lo è ancora di più se si considera che dobbiamo distribuire la maggior parte dei progetti in molte lingue diverse. I nostri contenuti devono funzionare non solo sui siti Web di BBC News e Sports, ma anche su app equivalenti su iOS e Android, nonché su siti di terze parti che utilizzano contenuti BBC.

Ora considera che c'è una gamma crescente di nuove piattaforme come AMP, Facebook Instant Articles e Apple News. Ogni piattaforma ha i suoi limiti e il suo meccanismo di pubblicazione proprietario. Creare contenuti interattivi che funzionino in tutti questi ambienti è una vera sfida. Descriverò come abbiamo affrontato il problema alla BBC.

Esempio: canonico vs. AMP

Tutto questo è un po' teorico finché non lo vedi in azione, quindi approfondiamo direttamente un esempio.

Ecco un articolo della BBC contenente contenuti di Visual Journalism:

Screenshot della pagina BBC News contenente contenuti di Visual Journalism
Il nostro contenuto di Visual Journalism inizia con l'illustrazione di Donald Trump e si trova all'interno di un iframe

Questa è la versione canonica dell'articolo, cioè la versione predefinita, che otterrai navigando all'articolo dalla home page.

Altro dopo il salto! Continua a leggere sotto ↓

Ora diamo un'occhiata alla versione AMP dell'articolo:

Screenshot della pagina AMP di BBC News contenente lo stesso contenuto di prima, ma il contenuto è ritagliato e ha un pulsante Mostra altro
Sembra lo stesso contenuto dell'articolo normale, ma sta inserendo un iframe diverso progettato specificamente per AMP

Sebbene le versioni canoniche e AMP abbiano lo stesso aspetto, in realtà sono due endpoint diversi con un comportamento diverso:

  • La versione canonica ti scorre nel paese prescelto quando invii il modulo.
  • La versione AMP non ti scorre, poiché non puoi scorrere la pagina principale da un iframe AMP.
  • La versione AMP mostra un iframe ritagliato con un pulsante "Mostra altro", a seconda delle dimensioni della finestra e della posizione di scorrimento. Questa è una caratteristica di AMP.

Oltre alle versioni canoniche e AMP di questo articolo, questo progetto è stato inviato anche all'app News, che è un'altra piattaforma con le sue complessità e limitazioni. Quindi , come supportiamo tutte queste piattaforme?

Gli strumenti sono fondamentali

Non costruiamo i nostri contenuti da zero. Abbiamo uno scaffold basato su Yeoman che utilizza Node per generare un progetto standard con un singolo comando.

I nuovi progetti vengono forniti con Webpack, SASS, implementazione e una struttura di componentizzazione pronta all'uso. L'internazionalizzazione è anche insita nei nostri progetti, utilizzando un sistema di sagomatura per Manubri. Tom Maslen ne parla in dettaglio nel suo post, 13 suggerimenti per rendere multilingue il responsive web design.

Per impostazione predefinita, funziona abbastanza bene per la compilazione per una piattaforma, ma è necessario supportare più piattaforme . Analizziamo un po' di codice.

Incorpora e autonomo

In Visual Journalism, a volte pubblichiamo il nostro contenuto all'interno di un iframe in modo che possa essere un "incorporamento" autonomo in un articolo, non influenzato dallo scripting e dallo stile globali. Un esempio di ciò è l'interattivo Donald Trump incorporato nell'esempio canonico in precedenza in questo articolo.

D'altra parte, a volte pubblichiamo il nostro contenuto come HTML grezzo. Lo facciamo solo quando abbiamo il controllo sull'intera pagina o se abbiamo bisogno di un'interazione di scorrimento davvero reattiva. Chiamiamoli rispettivamente i nostri output "embed" e "standalone".

Immaginiamo come potremmo costruire il "Un robot prenderà il tuo lavoro?" interattivo in entrambi i formati “embed” e “standalone”.

Due screenshot affiancati. Uno mostra il contenuto incorporato in una pagina; l'altro mostra lo stesso contenuto di una pagina a sé stante.
Esempio inventato che mostra un "incorpora" a sinistra, rispetto al contenuto come una pagina "autonoma" a destra

Entrambe le versioni del contenuto condividerebbero la stragrande maggioranza del loro codice, ma ci sarebbero alcune differenze cruciali nell'implementazione di JavaScript tra le due versioni.

Ad esempio, guarda il pulsante "Scopri il mio rischio di automazione". Quando l'utente preme il pulsante di invio, dovrebbe scorrere automaticamente fino ai risultati.

La versione "autonoma" del codice potrebbe assomigliare a questa:

 button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });

Ma se lo stai costruendo come output "incorpora", sai che il tuo contenuto è all'interno di un iframe, quindi dovresti codificarlo in modo diverso:

 // 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); } });

Inoltre, cosa succede se la nostra applicazione deve andare a schermo intero? Questo è abbastanza facile se ti trovi in ​​una pagina "autonoma":

 document.body.className += ' fullscreen';
 .fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } 
Screenshot della mappa incorporata con l'overlay "Tocca per interagire", seguito da uno screenshot della mappa in modalità a schermo intero dopo che è stata toccata.
Utilizziamo con successo la funzionalità a schermo intero per ottenere il massimo dal nostro modulo mappa sui dispositivi mobili

Se provassimo a farlo dall'interno di un "incorpora", questo stesso codice avrebbe il contenuto ridimensionato in base alla larghezza e all'altezza dell'iframe , piuttosto che al viewport:

Schermata dell'esempio di mappa come prima, ma la modalità a schermo intero è difettosa. Il testo dell'articolo circostante è visibile dove non dovrebbe essere.
Può essere difficile andare a schermo intero da un iframe

...quindi, oltre ad applicare lo stile a schermo intero all'interno dell'iframe, dobbiamo inviare un messaggio alla pagina host per applicare lo stile all'iframe stesso:

 // iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });

Questo può tradursi in un sacco di codice spaghetti quando inizi a supportare più piattaforme:

 button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });

Immagina di fare un equivalente di questo per ogni interazione DOM significativa nel tuo progetto. Una volta che hai finito di rabbrividire, preparati una tazza di tè rilassante e continua a leggere.

L'astrazione è la chiave

Invece di costringere i nostri sviluppatori a gestire queste condizioni all'interno del loro codice, abbiamo creato un livello di astrazione tra il loro contenuto e l'ambiente. Chiamiamo questo livello "involucro".

Invece di interrogare direttamente il DOM o gli eventi del browser nativo, ora possiamo inoltrare la nostra richiesta tramite il modulo wrapper .

 import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });

Ogni piattaforma ha la propria implementazione del wrapper conforme a un'interfaccia comune di metodi wrapper. Il wrapper si avvolge attorno al nostro contenuto e gestisce la complessità per noi.

Diagramma UML che mostra che quando la nostra applicazione chiama il metodo di scorrimento del wrapper autonomo, il wrapper chiama il metodo di scorrimento nativo nella pagina host.
Semplice implementazione "scrollTo" da parte del wrapper standalone

L'implementazione della funzione scrollTo da parte del wrapper autonomo è molto semplice, passando il nostro argomento direttamente a window.scrollTo sotto il cofano.

Ora diamo un'occhiata a un wrapper separato che implementa la stessa funzionalità per l'iframe:

Diagramma UML che mostra che quando la nostra applicazione chiama il metodo di scorrimento embed wrapper, quest'ultimo combina la posizione di scorrimento richiesta con l'offset dell'iframe prima di attivare il metodo di scorrimento nativo nella pagina host.
Implementazione avanzata di "scrollTo" da parte del wrapper di incorporamento

Il wrapper "incorpora" utilizza lo stesso argomento dell'esempio "autonomo", ma manipola il valore in modo che venga preso in considerazione l'offset iframe. Senza questa aggiunta, avremmo fatto scorrere il nostro utente da qualche parte completamente non intenzionale.

Il modello dell'involucro

L'uso dei wrapper produce un codice più pulito, più leggibile e coerente tra i progetti. Consente inoltre micro-ottimizzazioni nel tempo, poiché apportiamo miglioramenti incrementali ai wrapper per rendere i loro metodi più performanti e accessibili. Il tuo progetto può quindi beneficiare dell'esperienza di molti sviluppatori.

Allora, che aspetto ha un involucro?

Struttura dell'involucro

Ciascun wrapper comprende essenzialmente tre elementi: un modello Handlebars, un file JS del wrapper e un file SASS che denota uno stile specifico del wrapper. Inoltre, ci sono attività di compilazione che si collegano agli eventi esposti dall'impalcatura sottostante in modo che ogni wrapper sia responsabile della propria precompilazione e pulizia.

Questa è una vista semplificata del wrapper di incorporamento:

 embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss

Il nostro scaffolding sottostante espone il tuo modello di progetto principale come un Manubrio parziale, che viene consumato dal wrapper. Ad esempio, templates/wrapper.hbs potrebbe contenere:

 <div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>

scss/wrapper.scss contiene uno stile specifico del wrapper che il codice dell'applicazione non dovrebbe definire da sé. Il wrapper di incorporamento, ad esempio, replica molti degli stili di BBC News all'interno dell'iframe.

Infine, js/wrapper.js contiene l'implementazione iframed dell'API wrapper, dettagliata di seguito. Viene fornito separatamente al progetto, anziché essere compilato con il codice dell'applicazione: contrassegniamo il wrapper come globale nel processo di compilazione del Webpack. Ciò significa che, sebbene consegniamo la nostra applicazione a più piattaforme, compiliamo il codice solo una volta.

API wrapper

L'API wrapper astrae una serie di interazioni chiave del browser. Ecco i più importanti:

scrollTo(int)

Scorre alla posizione data nella finestra attiva. Il wrapper normalizzerà l'intero fornito prima di attivare lo scorrimento in modo che la pagina host venga fatta scorrere nella posizione corretta.

getScrollPosition: int

Restituisce la posizione di scorrimento corrente (normalizzata) dell'utente. Nel caso dell'iframe, ciò significa che la posizione di scorrimento passata all'applicazione è effettivamente negativa finché l'iframe non si trova nella parte superiore della finestra. Questo è super utile e ci consente di fare cose come animare un componente solo quando viene visualizzato.

onScroll(callback)

Fornisce un hook nell'evento di scorrimento. Nel wrapper autonomo, questo essenzialmente si collega all'evento di scorrimento nativo. Nel wrapper di incorporamento, si verificherà un leggero ritardo nella ricezione dell'evento di scorrimento poiché viene passato tramite postMessage.

viewport: {height: int, width: int}

Un metodo per recuperare l'altezza e la larghezza della finestra (poiché ciò viene implementato in modo molto diverso quando viene interrogato dall'interno di un iframe).

toggleFullScreen

In modalità standalone, nascondiamo alla vista il menu e il footer della BBC e impostiamo una position: fixed sul nostro contenuto. Nell'app News non facciamo nulla: il contenuto è già a schermo intero. Quello complicato è l'iframe, che si basa sull'applicazione di stili sia all'interno che all'esterno dell'iframe, coordinati tramite postMessage.

markPageAsLoaded

Dì al wrapper che il tuo contenuto è stato caricato. Questo è fondamentale per il funzionamento dei nostri contenuti nell'app News, che non tenterà di mostrare i nostri contenuti all'utente finché non comunicheremo esplicitamente all'app che i nostri contenuti sono pronti. Rimuove anche lo spinner di caricamento sulle versioni web dei nostri contenuti.

Elenco dei wrapper

In futuro, prevediamo di creare wrapper aggiuntivi per piattaforme di grandi dimensioni come Facebook Instant Articles e Apple News. Ad oggi abbiamo creato sei wrapper:

Involucro autonomo

La versione del nostro contenuto che dovrebbe andare in pagine autonome. Viene fornito in bundle con il marchio BBC.

Incorpora involucro

La versione iframe del nostro contenuto, che può essere inserita in modo sicuro all'interno degli articoli o da distribuire in syndication a siti non BBC, poiché manteniamo il controllo sul contenuto.

Involucro AMP

Questo è l'endpoint che viene inserito come amp-iframe nelle pagine AMP.

Avvolgi app di notizie

Il nostro contenuto deve effettuare chiamate a un protocollo proprietario bbcvisualjournalism:// .

Involucro centrale

Contiene solo l'HTML, nessuno dei CSS o JavaScript del nostro progetto.

Involucro JSON

Una rappresentazione JSON dei nostri contenuti, per la condivisione tra i prodotti BBC.

Avvolgitori di cablaggio fino alle piattaforme

Affinché i nostri contenuti appaiano sul sito della BBC, forniamo ai giornalisti un percorso con spazi di nomi:

 /include/[department]/[unique ID], eg /include/visual-journalism/123-quiz

Il giornalista inserisce questo "percorso di inclusione" nel CMS, che salva la struttura dell'articolo nel database. Tutti i prodotti e servizi si trovano a valle di questo meccanismo di pubblicazione. Ogni piattaforma è responsabile della scelta del contenuto desiderato e della richiesta di tale contenuto da un server proxy.

Prendiamo quel Donald Trump interattivo di prima. Qui, il percorso di inclusione nel CMS è:

 /include/newsspec/15996-trump-tracker/english/index

La pagina dell'articolo canonico sa che vuole la versione "incorporata" del contenuto, quindi aggiunge /embed al percorso di inclusione:

 /include/newsspec/15996-trump-tracker/english/index /embed

…prima di richiederlo al server proxy:

 https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed

La pagina AMP, d'altra parte, vede il percorso di inclusione e aggiunge /amp :

 /include/newsspec/15996-trump-tracker/english/index /amp

Il renderer AMP fa un po' di magia per rendere alcuni HTML AMP che fanno riferimento al nostro contenuto, inserendo la versione /amp come un 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>

Ogni piattaforma supportata ha la propria versione del contenuto:

 /include/newsspec/15996-trump-tracker/english/index /amp

/include/newsspec/15996-trump-tracker/english/index /core

/include/newsspec/15996-trump-tracker/english/index /envelope

...e così via

Questa soluzione può essere scalata per incorporare più tipi di piattaforma man mano che si presentano.

L'astrazione è difficile

Costruire un'architettura "scrivi una volta, distribuisci ovunque" sembra abbastanza idealistico, e lo è. Affinché l'architettura del wrapper funzioni, dobbiamo essere molto severi nel lavorare all'interno dell'astrazione. Ciò significa che dobbiamo combattere la tentazione di "fare questa cosa hacky per farlo funzionare in [inserire il nome della piattaforma qui]". Vogliamo che i nostri contenuti siano completamente all'oscuro dell'ambiente in cui vengono spediti, ma è più facile a dirsi che a farsi.

Le caratteristiche della piattaforma sono difficili da configurare in modo astratto

Prima del nostro approccio di astrazione, avevamo il controllo completo su ogni aspetto del nostro output, incluso, ad esempio, il markup del nostro iframe. Se avessimo bisogno di modificare qualcosa in base al progetto, come aggiungere un attributo title all'iframe per motivi di accessibilità, potremmo semplicemente modificare il markup.

Ora che il markup del wrapper esiste in isolamento dal progetto, l'unico modo per configurarlo sarebbe esporre un hook nello scaffold stesso. Possiamo farlo in modo relativamente semplice per le funzionalità multipiattaforma, ma esporre hook per piattaforme specifiche interrompe l'astrazione. Non vogliamo davvero esporre un'opzione di configurazione "titolo iframe" che viene utilizzata solo da un wrapper.

Potremmo nominare la proprietà in modo più generico, ad esempio title , e quindi utilizzare questo valore come attributo del title dell'iframe. Tuttavia, inizia a diventare difficile tenere traccia di ciò che viene utilizzato e dove, e rischiamo di atrarre la nostra configurazione al punto da non capirla più. In generale, cerchiamo di mantenere la nostra configurazione il più snella possibile, impostando solo proprietà che hanno un uso globale.

Il comportamento dei componenti può essere complesso

Sul Web, il nostro modulo sharetools emette pulsanti di condivisione sui social network che possono essere cliccati individualmente e aprono un messaggio di condivisione precompilato in una nuova finestra.

Screenshot della sezione degli strumenti di condivisione della BBC contenente le icone dei social media di Twitter e Facebook.
Gli strumenti di condivisione di BBC Visual Journalism presentano un elenco di opzioni di condivisione sui social

Nell'app News, non vogliamo condividere tramite il Web mobile. Se l'utente ha installato l'applicazione pertinente (ad es. Twitter), vogliamo condividere l'app stessa. Idealmente, vogliamo presentare all'utente il menu di condivisione nativo di iOS/Android, quindi lasciare che scelga l'opzione di condivisione prima di aprire l'app con un messaggio di condivisione precompilato. Possiamo attivare il menu di condivisione nativo dall'app effettuando una chiamata al protocollo proprietario bbcvisualjournalism:// .

Screenshot del menu di condivisione su Android con opzioni per la condivisione tramite Messaggistica, Bluetooth, Copia negli appunti e così via.
Menu di condivisione nativo su Android

Tuttavia, questa schermata verrà attivata toccando "Twitter" o "Facebook" nella sezione "Condividi i tuoi risultati", quindi l'utente finisce per dover fare la sua scelta due volte; la prima volta all'interno del nostro contenuto e una seconda volta sul popup nativo.

Questo è uno strano viaggio dell'utente, quindi vogliamo rimuovere le singole icone di condivisione dall'app News e mostrare invece un pulsante di condivisione generico. Siamo in grado di farlo controllando esplicitamente quale wrapper è in uso prima di eseguire il rendering del componente.

Screenshot del pulsante di condivisione dell'app di notizie. Questo è un unico pulsante con il seguente testo: "Condividi come hai fatto".
Pulsante di condivisione generico utilizzato nell'app News

La creazione del livello di astrazione del wrapper funziona bene per i progetti nel loro insieme, ma quando la scelta del wrapper influisce sulle modifiche a livello di componente , è molto difficile mantenere un'astrazione pulita. In questo caso, abbiamo perso un po' di astrazione e abbiamo una logica di fork disordinata nel nostro codice. Per fortuna, questi casi sono pochi e rari.

Come gestiamo le funzionalità mancanti?

Mantenere l'astrazione va bene. Il nostro codice dice al wrapper cosa vuole che la piattaforma faccia, ad esempio "vai a schermo intero". Ma cosa succede se la piattaforma su cui spediamo non può effettivamente andare a schermo intero?

Il wrapper farà del suo meglio per non rompersi del tutto, ma alla fine è necessario un design che ricada con grazia su una soluzione funzionante indipendentemente dal fatto che il metodo abbia successo o meno. Dobbiamo progettare in modo difensivo.

Supponiamo di avere una sezione dei risultati contenente alcuni grafici a barre. Spesso ci piace mantenere i valori del grafico a barre su zero fino a quando i grafici non vengono visualizzati, a quel punto attiviamo l'animazione delle barre alla larghezza corretta.

Screenshot di una raccolta di grafici a barre che confrontano l'area dell'utente con le medie nazionali. Ogni barra ha il suo valore visualizzato come testo a destra della barra.
Grafico a barre che mostra i valori rilevanti per la mia area

Ma se non abbiamo alcun meccanismo per agganciarci alla posizione di scorrimento, come nel caso del nostro wrapper AMP, le barre rimarrebbero per sempre a zero, il che è un'esperienza completamente fuorviante.

Stesso screenshot dei grafici a barre di prima, ma le barre hanno 0&#37; larghezza e i valori di ciascuna barra sono fissati a 0&#37;. Questo non è corretto.
Come potrebbe apparire il grafico a barre se gli eventi di scorrimento non vengono inoltrati

Cerchiamo sempre più di adottare un approccio di miglioramento progressivo nei nostri progetti. Ad esempio, potremmo fornire un pulsante che sarà visibile per tutte le piattaforme per impostazione predefinita, ma che viene nascosto se il wrapper supporta lo scorrimento. In questo modo, se lo scroll non riesce ad attivare l'animazione, l'utente può comunque attivare l'animazione manualmente.

Stesso screenshot dei grafici a barre dello 0&#37; grafici a barre, ma questa volta con una sottile sovrapposizione grigia e un pulsante centrato che invita l'utente a "Visualizza risultati".
Potremmo invece visualizzare un pulsante di fallback, che attiva l'animazione al clic.

Piani per il futuro

Ci auguriamo di sviluppare nuovi wrapper per piattaforme come Apple News e Facebook Instant Articles, nonché di offrire a tutte le nuove piattaforme una versione "core" dei nostri contenuti pronta all'uso.

Speriamo anche di migliorare nel miglioramento progressivo; avere successo in questo campo significa svilupparsi in modo difensivo. Non puoi mai presumere che tutte le piattaforme ora e in futuro supporteranno una determinata interazione, ma un progetto ben progettato dovrebbe essere in grado di trasmettere il suo messaggio centrale senza cadere nel primo ostacolo tecnico.

Lavorare entro i confini dell'involucro è un po' un cambio di paradigma e sembra un po' una via di mezzo in termini di soluzione a lungo termine . Ma fino a quando il settore non maturerà su uno standard multipiattaforma, gli editori saranno costretti a implementare le proprie soluzioni o utilizzare strumenti come Distro per la conversione da piattaforma a piattaforma, oppure ignorare del tutto intere sezioni del loro pubblico.

Per noi è l'inizio dei tempi, ma finora abbiamo avuto un grande successo nell'utilizzare il modello wrapper per creare i nostri contenuti una volta e distribuirli alla miriade di piattaforme che il nostro pubblico sta attualmente utilizzando.