Cum funcționează conținutul interactiv BBC pe AMP, aplicații și pe web

Publicat: 2022-03-10
Rezumat rapid ↬ Publicarea conținutului pe atât de multe medii fără o mulțime de cheltuieli suplimentare de dezvoltare poate fi dificilă. Chris Ashton explică cum au abordat problema în departamentul de jurnalism vizual al BBC.

În echipa de jurnalism vizual de la BBC, producem conținut vizual interesant, captivant și interactiv, de la calculatoare până la vizualizari, noi formate de povestire.

Fiecare aplicație este o provocare unică de produs în sine, dar cu atât mai mult atunci când considerați că trebuie să implementăm majoritatea proiectelor în multe limbi diferite. Conținutul nostru trebuie să funcționeze nu numai pe site-urile BBC News și Sports, ci și pe aplicațiile lor echivalente pe iOS și Android, precum și pe site-uri terțe care consumă conținut BBC.

Acum luați în considerare că există o gamă tot mai mare de noi platforme , cum ar fi AMP, Facebook Instant Articles și Apple News. Fiecare platformă are propriile sale limitări și mecanism de publicare proprietar. Crearea de conținut interactiv care funcționează în toate aceste medii este o adevărată provocare. O să descriu cum am abordat problema la BBC.

Exemplu: Canonical vs. AMP

Toate acestea sunt puțin teoretice până când îl vedeți în acțiune, așa că haideți să pătrundem direct într-un exemplu.

Iată un articol BBC care conține conținut de jurnalism vizual:

Captură de ecran a paginii BBC News care conține conținut de jurnalism vizual
Conținutul nostru de jurnalism vizual începe cu ilustrația lui Donald Trump și se află într-un iframe

Aceasta este versiunea canonică a articolului, adică versiunea implicită, pe care o veți obține dacă navigați la articol din pagina de pornire.

Mai multe după săritură! Continuați să citiți mai jos ↓

Acum să ne uităm la versiunea AMP a articolului:

Captură de ecran a paginii BBC News AMP care conține același conținut ca înainte, dar conținutul este decupat și are un buton Afișați mai multe
Acesta arată ca același conținut ca articolul obișnuit, dar include un iframe diferit conceput special pentru AMP

Deși versiunile canonice și AMP arată la fel, ele sunt de fapt două puncte finale diferite cu comportament diferit:

  • Versiunea canonică vă derulează la țara aleasă atunci când trimiteți formularul.
  • Versiunea AMP nu vă derulează, deoarece nu puteți derula pagina părinte dintr-un iframe AMP.
  • Versiunea AMP arată un iframe decupat cu un buton „Afișați mai multe”, în funcție de dimensiunea ferestrei de vizualizare și de poziția de defilare. Aceasta este o caracteristică a AMP.

Pe lângă versiunile canonice și AMP ale acestui articol, acest proiect a fost livrat și către aplicația de știri, care este încă o platformă cu propriile sale complexități și limitări. Deci , cum susținem toate aceste platforme?

Instrumentul este cheia

Nu ne construim conținutul de la zero. Avem o schelă bazată pe Yeoman care folosește Node pentru a genera un proiect standard cu o singură comandă.

Noile proiecte vin cu Webpack, SASS, implementare și o structură de componentizare din cutie. Internaționalizarea este, de asemenea, inclusă în proiectele noastre, folosind un sistem de șabloane Handlebars. Tom Maslen scrie despre acest lucru în detaliu în postarea sa, 13 sfaturi pentru a face un web design responsive multilingv.

Din cutie, acest lucru funcționează destul de bine pentru compilarea pentru o singură platformă, dar trebuie să acceptăm mai multe platforme . Să pătrundem într-un cod.

Încorporare vs. Standalone

În jurnalismul vizual, uneori ne trimitem conținutul într-un iframe, astfel încât să poată fi un „încorporat” autonom într-un articol, neafectat de scriptingul și stilul global. Un exemplu în acest sens este interactivul Donald Trump încorporat în exemplul canonic de mai devreme în acest articol.

Pe de altă parte, uneori ne scoatem conținutul ca HTML brut. Facem acest lucru doar atunci când avem control asupra întregii pagini sau dacă avem nevoie de o interacțiune cu derulare cu adevărat receptivă. Să le numim ieșirile noastre „încorporate” și, respectiv, „autonome”.

Să ne imaginăm cum am putea construi „Vă va lua un robot slujba?” interactiv atât în ​​formatul „încorporat”, cât și în formatul „autonom”.

Două capturi de ecran una lângă alta. Unul arată conținutul încorporat într-o pagină; celălalt arată același conținut ca o pagină în sine.
Exemplu inventat care arată o „încorporare” în stânga, comparativ cu conținutul ca o pagină „autonomă” în partea dreaptă

Ambele versiuni ale conținutului ar împărtăși marea majoritate a codului lor, dar ar exista unele diferențe cruciale în implementarea JavaScript între cele două versiuni.

De exemplu, uitați-vă la butonul „Aflați riscul meu de automatizare”. Când utilizatorul apasă butonul de trimitere, ar trebui să fie derulat automat la rezultate.

Versiunea „autonomă” a codului ar putea arăta astfel:

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

Dar dacă ați construi acest lucru ca ieșire „încorporată”, știți că conținutul dvs. se află într-un iframe, așa că ar trebui să îl codificați diferit:

 // 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 asemenea, ce se întâmplă dacă aplicația noastră trebuie să treacă pe ecran complet? Acest lucru este destul de ușor dacă vă aflați într-o pagină „autonomă”:

 document.body.className += ' fullscreen';
 .fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } 
Captură de ecran a hărții încorporate cu suprapunerea „Atingeți pentru a interacționa”, urmată de o captură de ecran a hărții în modul ecran complet după ce a fost atinsă.
Folosim cu succes funcționalitatea pe ecran complet pentru a profita la maximum de modulul nostru de hărți pe mobil

Dacă am încerca să facem acest lucru din interiorul unui „încorporat”, același cod ar avea conținutul scalat la lățimea și înălțimea iframe -ului, mai degrabă decât la portul de vizualizare:

Captură de ecran a unui exemplu de hărți ca înainte, dar modul ecran complet este greșit. Textul din articolul din jur este vizibil acolo unde nu ar trebui să fie.
Poate fi dificil să accesați ecranul complet dintr-un iframe

…deci, pe lângă aplicarea stilului pe ecran complet în interiorul iframe-ului, trebuie să trimitem un mesaj către pagina gazdă pentru a aplica stilul iframe-ului în sine:

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

Acest lucru se poate traduce într-o mulțime de cod spaghetti atunci când începeți să susțineți mai multe platforme:

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

Imaginați-vă că faceți un echivalent pentru fiecare interacțiune DOM semnificativă din proiectul dvs. Odată ce ai terminat de înfiorat, fă-ți o ceașcă de ceai relaxantă și citește mai departe.

Abstracția este cheia

În loc să ne forțăm dezvoltatorii să gestioneze aceste condiții în codul lor, am construit un strat de abstractizare între conținutul lor și mediu. Numim acest strat „înveliș”.

În loc să interogăm direct DOM-ul sau evenimentele browserului nativ, acum ne putem proxy solicitarea prin modulul wrapper .

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

Fiecare platformă are propria sa implementare wrapper, conformă cu o interfață comună a metodelor wrapper-ului. Wrapper-ul se înfășoară în jurul conținutului nostru și gestionează complexitatea pentru noi.

Diagrama UML care arată că atunci când aplicația noastră apelează metoda de defilare a wrapper-ului independent, wrapper-ul apelează metoda de defilare nativă în pagina gazdă.
Implementare simplă „scrollTo” de către wrapper-ul independent

Implementarea de către wrapper-ul independent a funcției scrollTo este foarte simplă, trecând argumentul nostru direct la window.scrollTo sub capotă.

Acum să ne uităm la un wrapper separat care implementează aceeași funcționalitate pentru iframe:

Diagrama UML care arată că atunci când aplicația noastră apelează metoda de defilare a învelișului încorporat, învelișul încorporat combină poziția de defilare solicitată cu decalajul iframe-ului înainte de a declanșa metoda de defilare nativă în pagina gazdă.
Implementare avansată „scrollTo” de către wrapper-ul încorporat

Wrapper-ul „embed” ia același argument ca în exemplul „autonom”, dar manipulează valoarea astfel încât să fie luată în considerare decalajul iframe. Fără această adăugare, ne-am fi derulat utilizatorul undeva complet neintenționat.

Modelul Wrapper

Utilizarea wrapper-urilor are ca rezultat un cod mai curat, mai lizibil și mai consistent între proiecte. De asemenea, permite micro-optimizări de-a lungul timpului, deoarece aducem îmbunătățiri incrementale wrapper-urilor pentru a le face metodele mai performante și mai accesibile. Prin urmare, proiectul dumneavoastră poate beneficia de experiența multor dezvoltatori.

Deci, cum arată un ambalaj?

Structura Wrapper

Fiecare wrapper cuprinde în esență trei lucruri: un șablon de ghidon, un fișier JS de wrapper și un fișier SASS care denotă stilul specific wrapper-ului. În plus, există sarcini de compilare care se conectează la evenimente expuse de schelele de bază, astfel încât fiecare înveliș să fie responsabil pentru propria sa pre-compilare și curățare.

Aceasta este o vedere simplificată a învelișului încorporat:

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

Schela noastră de bază expune șablonul principal al proiectului ca un parțial Ghidon, care este consumat de ambalaj. De exemplu, templates/wrapper.hbs poate conține:

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

scss/wrapper.scss conține stiluri specifice wrapper-ului pe care codul aplicației nu ar trebui să-l definească. Învelișul încorporat, de exemplu, reproduce o mulțime de stiluri BBC News în interiorul iframe-ului.

În cele din urmă, js/wrapper.js conține implementarea iframed a API-ului wrapper, detaliată mai jos. Este livrat separat în proiect, mai degrabă decât compilat cu codul aplicației - semnalăm wrapper -ul ca global în procesul nostru de construire Webpack. Aceasta înseamnă că, deși livrăm aplicația noastră pe mai multe platforme, compilam codul o singură dată.

Wrapper API

API-ul wrapper retrage o serie de interacțiuni cheie ale browserului. Iată cele mai importante:

scrollTo(int)

Derulează la poziția dată în fereastra activă. Wrapper-ul va normaliza întregul furnizat înainte de a declanșa derularea, astfel încât pagina gazdă să fie derulată în poziția corectă.

getScrollPosition: int

Returnează poziția de defilare curentă (normalizată) a utilizatorului. În cazul iframe-ului, aceasta înseamnă că poziția de defilare transmisă aplicației dvs. este de fapt negativă până când iframe-ul se află în partea de sus a ferestrei de vizualizare. Acest lucru este foarte util și ne permite să facem lucruri precum animarea unei componente numai atunci când aceasta este vizibilă.

onScroll(callback)

Oferă un cârlig în evenimentul de defilare. În wrapper-ul de sine stătător, acesta este, în esență, conectat la evenimentul nativ de defilare. În ambalajul încorporat, va exista o ușoară întârziere în primirea evenimentului de defilare, deoarece acesta este transmis prin postMessage.

viewport: {height: int, width: int}

O metodă de a prelua înălțimea și lățimea ferestrei de vizualizare (deoarece aceasta este implementată foarte diferit atunci când este interogat dintr-un iframe).

toggleFullScreen

În modul de sine stătător, ascundem meniul BBC și subsolul din vedere și setăm o position: fixed ​​pe conținutul nostru. În aplicația Știri, nu facem nimic – conținutul este deja pe ecran complet. Cel complicat este iframe, care se bazează pe aplicarea stilurilor atât în ​​interiorul cât și în exteriorul iframe-ului, coordonate prin postMessage.

markPageAsLoaded

Spuneți wrapper-ului conținutul dvs. încărcat. Acest lucru este esențial pentru ca conținutul nostru să funcționeze în aplicația Știri, care nu va încerca să afișeze conținutul nostru pentru utilizator până când nu spunem în mod explicit aplicației că conținutul nostru este gata. De asemenea, elimină rotorul de încărcare de pe versiunile web ale conținutului nostru.

Lista de ambalaje

În viitor, avem în vedere crearea de pachete suplimentare pentru platforme mari, cum ar fi Facebook Instant Articles și Apple News. Până în prezent, am creat șase ambalaje:

Ambalaj autonom

Versiunea conținutului nostru care ar trebui să apară în pagini independente. Vine la pachet cu branding BBC.

Încorporați Wrapper

Versiunea iframed a conținutului nostru, care este sigură pentru a se așeza în articole sau pentru a distribui către site-uri non-BBC, deoarece păstrăm controlul asupra conținutului.

AMP Wrapper

Acesta este punctul final care este introdus ca amp-iframe în paginile AMP.

News App Wrapper

Conținutul nostru trebuie să facă apeluri către un protocol proprietar bbcvisualjournalism:// .

Înveliș de miez

Conține doar HTML - niciunul dintre CSS sau JavaScript ale proiectului nostru.

Wrapper JSON

O reprezentare JSON a conținutului nostru, pentru partajarea produselor BBC.

Învelișuri de cablare până la platforme

Pentru ca conținutul nostru să apară pe site-ul BBC, oferim jurnaliștilor o cale cu spații de nume:

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

Jurnalistul introduce această „cale de includere” în CMS, care salvează structura articolului în baza de date. Toate produsele și serviciile se află în aval de acest mecanism de publicare. Fiecare platformă este responsabilă pentru alegerea conținutului pe care îl dorește și pentru a solicita acel conținut de la un server proxy.

Să luăm acel Donald Trump interactiv de mai devreme. Aici, calea include în CMS este:

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

Pagina articolului canonic știe că dorește versiunea „încorporată” a conținutului, așa că adaugă /embed la calea include:

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

… înainte de a-l solicita de la serverul proxy:

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

Pagina AMP, pe de altă parte, vede calea include și adaugă /amp :

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

Redarea AMP face puțină magie pentru a reda niște coduri HTML AMP care face referire la conținutul nostru, introducând versiunea /amp ca 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>

Fiecare platformă acceptată are propria sa versiune a conținutului:

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

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

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

...și așa mai departe

Această soluție se poate scala pentru a încorpora mai multe tipuri de platforme pe măsură ce apar.

Abstracția este grea

Construirea unei arhitecturi „scrie o dată, implementează oriunde” sună destul de idealist și este. Pentru ca arhitectura wrapper-ului să funcționeze, trebuie să fim foarte stricti în ceea ce privește lucrul în cadrul abstracției. Acest lucru înseamnă că trebuie să luptăm cu tentația de a „face acest lucru hacky pentru a-l face să funcționeze în [inserați numele platformei aici]”. Dorim ca conținutul nostru să nu fie complet conștient de mediul în care este expediat - dar acest lucru este mai ușor de spus decât de făcut.

Caracteristicile platformei sunt greu de configurat în mod abstract

Înainte de abordarea noastră de abstractizare, aveam control complet asupra fiecărui aspect al ieșirii noastre, inclusiv, de exemplu, marcajul iframe-ului nostru. Dacă ar fi nevoie să modificăm ceva pe fiecare proiect, cum ar fi adăugarea unui atribut de title la iframe din motive de accesibilitate, am putea doar să edităm marcajul.

Acum că marcajul wrapper-ului există izolat de proiect, singura modalitate de a-l configura ar fi expunerea unui cârlig în schela în sine. Putem face acest lucru relativ ușor pentru caracteristicile multiplatforme, dar expunerea cârligelor pentru anumite platforme rupe abstractizarea. Nu vrem cu adevărat să expunem o opțiune de configurare „iframe title” care este folosită doar de un singur wrapper.

Am putea denumi proprietatea în mod mai generic, de exemplu title , și apoi să folosim această valoare ca atribut title iframe. Totuși, începe să devină dificil să ținem evidența a ceea ce este folosit unde și riscăm să ne abstragem configurația până la punctul de a nu o mai înțelege. În general, încercăm să ne păstrăm configurația cât mai slabă posibil, setând doar proprietăți care au o utilizare globală.

Comportamentul componentelor poate fi complex

Pe web, modulul nostru Sharetools scuipă butoane de partajare a rețelelor sociale pe care se poate face clic individual și deschide un mesaj de partajare pre-populat într-o fereastră nouă.

Captură de ecran a secțiunii BBC Sharetools care conține pictograme pentru rețelele sociale Twitter și Facebook.
BBC Visual Journalism sharetools prezintă o listă de opțiuni de distribuire socială

În aplicația Știri, nu dorim să distribuim prin web mobil. Dacă utilizatorul are instalată aplicația relevantă (de ex. Twitter), dorim să partajăm aplicația în sine. În mod ideal, dorim să prezentăm utilizatorului meniul de partajare nativ iOS/Android, apoi să-i lăsăm să aleagă opțiunea de partajare înainte de a deschide aplicația pentru el cu un mesaj de partajare pre-populat. Putem declanșa meniul de partajare nativ din aplicație făcând un apel la protocolul proprietar bbcvisualjournalism:// .

Captură de ecran a meniului de partajare pe Android cu opțiuni de partajare prin Mesaje, Bluetooth, Copiere în clipboard și așa mai departe.
Meniul de partajare nativ pe Android

Cu toate acestea, acest ecran va fi declanșat indiferent dacă atingeți „Twitter” sau „Facebook” în secțiunea „Partajați rezultatele”, astfel încât utilizatorul va trebui să facă alegerea de două ori; prima dată în conținutul nostru și a doua oară în popup-ul nativ.

Aceasta este o călătorie ciudată a utilizatorului, așa că dorim să eliminăm pictogramele de distribuire individuale din aplicația Știri și să arătăm în schimb un buton de distribuire generic. Putem face acest lucru verificând în mod explicit ce wrapper este utilizat înainte de a reda componenta.

Captură de ecran a butonului de partajare a aplicației de știri. Acesta este un singur buton cu următorul text: „Distribuie cum ai făcut”.
Buton de partajare generic folosit în aplicația Știri

Construirea stratului de abstracție a wrapperului funcționează bine pentru proiecte în ansamblu, dar atunci când alegerea dvs. de wrapper afectează modificări la nivel de componentă , este foarte dificil să păstrați o abstractizare curată. În acest caz, am pierdut puțină abstracție și avem o logică dezordonată în codul nostru. Din fericire, aceste cazuri sunt puține și îndepărtate.

Cum gestionăm funcțiile lipsă?

Păstrarea abstracției este bine și bine. Codul nostru îi spune wrapper-ului ce dorește să facă platforma, de exemplu, „mergi pe ecran complet”. Dar ce se întâmplă dacă platforma către care trimitem nu poate merge de fapt pe ecran complet?

Ambalajul va face tot posibilul să nu se rupă cu totul, dar în cele din urmă aveți nevoie de un design care să se întoarcă cu grație la o soluție de lucru, indiferent dacă metoda reușește sau nu. Trebuie să proiectăm defensiv.

Să presupunem că avem o secțiune de rezultate care conține niște diagrame cu bare. Adesea ne place să menținem valorile diagramelor cu bare la zero până când diagramele sunt derulate în vedere, moment în care declanșăm barele care se anima la lățimea lor corectă.

Captură de ecran a unei colecții de diagrame cu bare care compară zona utilizatorului cu mediile naționale. Fiecare bară are valoarea sa afișată ca text în partea dreaptă a barei.
Diagramă cu bare care arată valorile relevante pentru zona mea

Dar dacă nu avem niciun mecanism pentru a ne conecta în poziția de defilare - așa cum este cazul în ambalajul nostru AMP - atunci barele ar rămâne pentru totdeauna la zero, ceea ce este o experiență complet înșelătoare.

Aceeași captură de ecran a diagramelor cu bare ca înainte, dar barele au 0&#37; lățimea și valorile fiecărei bare sunt fixate la 0&#37;. Acest lucru este incorect.
Cum ar putea arăta diagrama cu bare dacă evenimentele de derulare nu sunt redirecționate

Încercăm din ce în ce mai mult să adoptăm o abordare de îmbunătățire progresivă în design-urile noastre. De exemplu, am putea furniza un buton care va fi vizibil pentru toate platformele în mod implicit, dar care este ascuns dacă wrapper-ul acceptă derularea. În acest fel, dacă derularea nu reușește să declanșeze animația, utilizatorul poate încă declanșa animația manual.

Aceeași captură de ecran a diagramelor cu bare ca și 0&#37; diagrame cu bare, dar de data aceasta cu o suprapunere subtilă de gri și un buton centrat care invită utilizatorul să „Vede rezultatele”.
În schimb, am putea afișa un buton de rezervă, care declanșează animația la clic.

Planuri de viitor

Sperăm să dezvoltăm noi wrapper-uri pentru platforme precum Apple News și Facebook Instant Articles, precum și să oferim tuturor platformelor noi o versiune „de bază” a conținutului nostru.

De asemenea, sperăm să ne îmbunătățim la îmbunătățirea progresivă; a reuși în acest domeniu înseamnă a te dezvolta defensiv. Nu puteți presupune niciodată că toate platformele acum și în viitor vor sprijini o anumită interacțiune, dar un proiect bine conceput ar trebui să poată transmite mesajul său de bază fără a cădea la primul obstacol tehnic.

Lucrul în limitele ambalajului este un pic o schimbare de paradigmă și se simte ca o casă de jumătate în ceea ce privește soluția pe termen lung . Dar până când industria ajunge la un standard multiplatform, editorii vor fi forțați să-și lanseze propriile soluții sau să folosească instrumente precum Distro pentru conversia de la platformă la platformă, sau să ignore cu totul secțiuni întregi ale publicului lor.

Sunt primele zile pentru noi, dar până acum am avut un mare succes în utilizarea modelului wrapper pentru a ne construi conținutul o dată și a-l livra pe nenumăratele platforme pe care publicul nostru le folosește acum.