Cómo funciona el contenido interactivo de la BBC en AMP, aplicaciones y la Web
Publicado: 2022-03-10En el equipo de Periodismo Visual de la BBC, producimos contenido visual emocionante, atractivo e interactivo, que va desde calculadoras hasta visualizaciones, nuevos formatos de narración.
Cada aplicación es un desafío único para producir por derecho propio, pero aún más cuando se considera que tenemos que implementar la mayoría de los proyectos en muchos idiomas diferentes. Nuestro contenido tiene que funcionar no solo en los sitios web de noticias y deportes de la BBC, sino también en sus aplicaciones equivalentes en iOS y Android, así como en sitios de terceros que consumen contenido de la BBC.
Ahora considere que hay una variedad cada vez mayor de nuevas plataformas como AMP, Facebook Instant Articles y Apple News. Cada plataforma tiene sus propias limitaciones y mecanismo de publicación propietario. Crear contenido interactivo que funcione en todos estos entornos es un verdadero desafío. Voy a describir cómo hemos abordado el problema en la BBC.
Ejemplo: Canonical vs. AMP
Todo esto es un poco teórico hasta que lo ves en acción, así que profundicemos directamente en un ejemplo.
Aquí hay un artículo de la BBC que contiene contenido de periodismo visual:
Esta es la versión canónica del artículo, es decir, la versión predeterminada, que obtendrá si navega hasta el artículo desde la página de inicio.
Ahora veamos la versión AMP del artículo:
Si bien las versiones canónica y AMP tienen el mismo aspecto, en realidad son dos puntos finales diferentes con un comportamiento diferente:
- La versión canónica lo desplaza a su país elegido cuando envía el formulario.
- La versión de AMP no te desplaza, ya que no puedes desplazarte por la página principal desde un iframe de AMP.
- La versión AMP muestra un iframe recortado con un botón "Mostrar más", según el tamaño de la ventana gráfica y la posición de desplazamiento. Esta es una característica de AMP.
Además de las versiones canónica y AMP de este artículo, este proyecto también se envió a la aplicación News, que es otra plataforma con sus propias complejidades y limitaciones. Entonces , ¿cómo admitimos todas estas plataformas?
El herramental es clave
No construimos nuestro contenido desde cero. Tenemos un andamio basado en Yeoman que usa Node para generar un proyecto repetitivo con un solo comando.
Los nuevos proyectos vienen con Webpack, SASS, implementación y una estructura de componentes lista para usar. La internacionalización también está integrada en nuestros proyectos, utilizando un sistema de plantillas de Handlebars. Tom Maslen escribe sobre esto en detalle en su publicación, 13 consejos para hacer que el diseño web receptivo sea multilingüe.
Fuera de la caja, esto funciona bastante bien para compilar para una plataforma, pero necesitamos admitir múltiples plataformas . Profundicemos en algo de código.
Incrustado vs Independiente
En el periodismo visual, a veces mostramos nuestro contenido dentro de un iframe para que pueda ser un "incrustado" autónomo en un artículo, sin verse afectado por el scripting y el estilo global. Un ejemplo de esto es el interactivo de Donald Trump incrustado en el ejemplo canónico anterior en este artículo.
Por otro lado, a veces mostramos nuestro contenido como HTML sin procesar. Solo hacemos esto cuando tenemos control sobre toda la página o si requerimos una interacción de desplazamiento realmente receptiva. Llamemos a estas nuestras salidas "incrustadas" e "independientes" respectivamente.
Imaginemos cómo podríamos construir la pregunta "¿Un robot te quitará el trabajo?" interactivo en los formatos "incrustado" e "independiente".
Ambas versiones del contenido compartirían la gran mayoría de su código, pero habría algunas diferencias cruciales en la implementación de JavaScript entre las dos versiones.
Por ejemplo, mire el botón 'Descubra mi riesgo de automatización'. Cuando el usuario presiona el botón Enviar, debe desplazarse automáticamente a sus resultados.
La versión "independiente" del código podría verse así:
button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });
Pero si estuviera creando esto como salida "incrustada", sabe que su contenido está dentro de un iframe, por lo que tendría que codificarlo de manera diferente:
// 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); } });
Además, ¿qué pasa si nuestra aplicación necesita pasar a pantalla completa? Esto es bastante fácil si estás en una página "independiente":
document.body.className += ' fullscreen';
.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }
Si tratáramos de hacer esto desde dentro de una "incrustación", este mismo código tendría el contenido escalando al ancho y alto del iframe , en lugar de la ventana gráfica:
…entonces, además de aplicar el estilo de pantalla completa dentro del iframe, tenemos que enviar un mensaje a la página de host para aplicar el estilo al propio iframe:
// iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });
Esto puede traducirse en una gran cantidad de código espagueti cuando comienza a admitir múltiples plataformas:
button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });
Imagina hacer un equivalente de esto para cada interacción DOM significativa en tu proyecto. Una vez que haya terminado de estremecerse, prepárese una taza de té relajante y siga leyendo.
La abstracción es clave
En lugar de obligar a nuestros desarrolladores a manejar estos condicionales dentro de su código, creamos una capa de abstracción entre su contenido y el entorno. Llamamos a esta capa el 'envoltorio'.
En lugar de consultar el DOM o los eventos del navegador nativo directamente, ahora podemos enviar nuestra solicitud a través del módulo wrapper
.
import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });
Cada plataforma tiene su propia implementación de contenedor conforme a una interfaz común de métodos de contenedor. El contenedor envuelve nuestro contenido y maneja la complejidad por nosotros.
La implementación del contenedor independiente de la función scrollTo
es muy simple, pasando nuestro argumento directamente a window.scrollTo
debajo del capó.
Ahora veamos un contenedor separado que implementa la misma funcionalidad para el iframe:
El contenedor "incrustado" toma el mismo argumento que en el ejemplo "independiente", pero manipula el valor para que se tenga en cuenta el desplazamiento del iframe. Sin esta adición, hubiéramos desplazado a nuestro usuario a algún lugar completamente involuntario.
El patrón de envoltura
El uso de contenedores da como resultado un código más limpio, más legible y consistente entre proyectos. También permite microoptimizaciones a lo largo del tiempo, a medida que realizamos mejoras incrementales en los envoltorios para hacer que sus métodos sean más eficaces y accesibles. Su proyecto puede, por lo tanto, beneficiarse de la experiencia de muchos desarrolladores.
Entonces, ¿cómo es un envoltorio?
Estructura de envoltura
Cada envoltorio consta esencialmente de tres cosas: una plantilla de manillar, un archivo JS de envoltorio y un archivo SASS que indica un estilo específico del envoltorio. Además, hay tareas de compilación que se conectan a eventos expuestos por el andamiaje subyacente para que cada contenedor sea responsable de su propia precompilación y limpieza.
Esta es una vista simplificada del envoltorio incrustado:
embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss
Nuestro andamiaje subyacente expone la plantilla de su proyecto principal como un Handlebars parcial, que es consumido por el contenedor. Por ejemplo, templates/wrapper.hbs
podría contener:
<div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>
scss/wrapper.scss
contiene un estilo específico de contenedor que el código de su aplicación no debería necesitar definir por sí mismo. El envoltorio incrustado, por ejemplo, replica mucho estilo de BBC News dentro del iframe.
Finalmente, js/wrapper.js
contiene la implementación iframed de la API contenedora, que se detalla a continuación. Se envía por separado al proyecto, en lugar de compilarse con el código de la aplicación; marcamos el wrapper
como global en nuestro proceso de creación de Webpack. Esto significa que aunque entregamos nuestra aplicación a múltiples plataformas, solo compilamos el código una vez.
API contenedora
La API contenedora abstrae una serie de interacciones clave del navegador. Aquí están los más importantes:
scrollTo(int)
Se desplaza a la posición dada en la ventana activa. El contenedor normalizará el entero proporcionado antes de activar el desplazamiento para que la página del host se desplace a la posición correcta.
getScrollPosition: int
Devuelve la posición de desplazamiento actual (normalizada) del usuario. En el caso del iframe, esto significa que la posición de desplazamiento pasada a su aplicación es realmente negativa hasta que el iframe esté en la parte superior de la ventana gráfica. Esto es súper útil y nos permite hacer cosas como animar un componente solo cuando está a la vista.
onScroll(callback)
Proporciona un gancho en el evento de desplazamiento. En el contenedor independiente, esto se conecta esencialmente al evento de desplazamiento nativo. En el envoltorio de incrustación, habrá un ligero retraso en la recepción del evento de desplazamiento, ya que se pasa a través de postMessage.
viewport: {height: int, width: int}
Un método para recuperar la altura y el ancho de la ventana gráfica (ya que esto se implementa de manera muy diferente cuando se consulta desde dentro de un iframe).
toggleFullScreen
En el modo independiente, ocultamos el menú y el pie de página de la BBC y establecemos una position: fixed
en nuestro contenido. En la aplicación de noticias, no hacemos nada en absoluto: el contenido ya está en pantalla completa. El complicado es el iframe, que se basa en la aplicación de estilos tanto dentro como fuera del iframe, coordinados a través de postMessage.
markPageAsLoaded
Dígale al contenedor que su contenido se ha cargado. Esto es crucial para que nuestro contenido funcione en la aplicación de noticias, que no intentará mostrar nuestro contenido al usuario hasta que le digamos explícitamente a la aplicación que nuestro contenido está listo. También elimina la rueda giratoria de carga en las versiones web de nuestro contenido.
Lista de envoltorios
En el futuro, tenemos previsto crear envoltorios adicionales para grandes plataformas como Facebook Instant Articles y Apple News. Hemos creado seis envoltorios hasta la fecha:
Envoltura independiente
La versión de nuestro contenido que debe ir en páginas independientes. Viene incluido con la marca BBC.
Envoltorio incrustado
La versión iframed de nuestro contenido, que es segura para colocarse dentro de los artículos o distribuirse a sitios que no pertenecen a la BBC, ya que retenemos el control sobre el contenido.
Envoltorio AMP
Este es el punto final que se introduce como un amp-iframe
en las páginas de AMP.
Envoltura de aplicaciones de noticias
Nuestro contenido debe hacer llamadas a un protocolo propietario bbcvisualjournalism://
.
Envoltura de núcleo
Contiene solo el HTML, ninguno de los CSS o JavaScript de nuestro proyecto.
Envoltorio JSON
Una representación JSON de nuestro contenido, para compartir entre los productos de la BBC.
Envolvedores de cableado hasta las plataformas
Para que nuestro contenido aparezca en el sitio de la BBC, proporcionamos a los periodistas una ruta con espacio de nombres:
/include/[department]/[unique ID], eg
/include/visual-journalism/123-quiz
El periodista coloca esta "ruta de inclusión" en el CMS, que guarda la estructura del artículo en la base de datos. Todos los productos y servicios se encuentran aguas abajo de este mecanismo de publicación. Cada plataforma es responsable de elegir el tipo de contenido que desea y solicitar ese contenido a un servidor proxy.
Tomemos ese Donald Trump interactivo de antes. Aquí, la ruta de inclusión en el CMS es:
/include/newsspec/15996-trump-tracker/english/index
La página del artículo canónico sabe que quiere la versión "incrustada" del contenido, por lo que agrega /embed
a la ruta de inclusión:
/include/newsspec/15996-trump-tracker/english/index
/embed
…antes de solicitarlo al servidor proxy:
https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed
La página de AMP, por otro lado, ve la ruta de inclusión y agrega /amp
:
/include/newsspec/15996-trump-tracker/english/index
/amp
El renderizador de AMP hace un poco de magia para renderizar algo de HTML de AMP que hace referencia a nuestro contenido, extrayendo la versión /amp
como 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>
Cada plataforma compatible tiene su propia versión del contenido:
/include/newsspec/15996-trump-tracker/english/index
/amp
/include/newsspec/15996-trump-tracker/english/index
/core
/include/newsspec/15996-trump-tracker/english/index
/envelope
...y así
Esta solución puede escalar para incorporar más tipos de plataforma a medida que surjan.
La abstracción es difícil
Construir una arquitectura de "escribir una vez, implementar en cualquier lugar" suena bastante idealista, y lo es. Para que la arquitectura contenedora funcione, debemos ser muy estrictos al trabajar dentro de la abstracción. Esto significa que tenemos que luchar contra la tentación de "hacer este truco para que funcione en [inserte el nombre de la plataforma aquí]". Queremos que nuestro contenido desconozca por completo el entorno en el que se envía, pero es más fácil decirlo que hacerlo.
Las características de la plataforma son difíciles de configurar de forma abstracta
Antes de nuestro enfoque de abstracción, teníamos control total sobre todos los aspectos de nuestra salida, incluido, por ejemplo, el marcado de nuestro iframe. Si necesitáramos modificar algo por proyecto, como agregar un atributo de title
al iframe por razones de accesibilidad, podríamos simplemente editar el marcado.
Ahora que el marcado contenedor existe de forma aislada del proyecto, la única forma de configurarlo sería exponer un enlace en el propio andamio. Podemos hacer esto con relativa facilidad para funciones multiplataforma, pero exponer ganchos para plataformas específicas rompe la abstracción. Realmente no queremos exponer una opción de configuración de 'título de iframe' que solo es utilizada por un envoltorio.
Podríamos nombrar la propiedad de manera más genérica, por ejemplo, title
, y luego usar este valor como el atributo de title
de iframe. Sin embargo, comienza a ser difícil hacer un seguimiento de qué se usa y dónde, y corremos el riesgo de abstraer nuestra configuración hasta el punto de dejar de entenderla. En general, tratamos de mantener nuestra configuración lo más simple posible, solo configuramos propiedades que tienen un uso global.
El comportamiento de los componentes puede ser complejo
En la web, nuestro módulo de herramientas para compartir escupe botones para compartir en redes sociales en los que se puede hacer clic individualmente y abre un mensaje para compartir rellenado previamente en una nueva ventana.
En la aplicación de noticias, no queremos compartir a través de la web móvil. Si el usuario tiene instalada la aplicación correspondiente (por ejemplo, Twitter), queremos compartir en la propia aplicación. Idealmente, queremos presentarle al usuario el menú compartido nativo de iOS/Android, luego dejar que elija su opción de compartir antes de que abramos la aplicación para ellos con un mensaje de compartir previamente completado. Podemos activar el menú compartido nativo desde la aplicación haciendo una llamada al protocolo propietario bbcvisualjournalism://
.
Sin embargo, esta pantalla se activará si toca 'Twitter' o 'Facebook' en la sección 'Comparte tus resultados', por lo que el usuario termina teniendo que hacer su elección dos veces; la primera vez dentro de nuestro contenido, y la segunda vez en la ventana emergente nativa.
Este es un viaje de usuario extraño, por lo que queremos eliminar los íconos de compartir individuales de la aplicación News y mostrar un botón de compartir genérico en su lugar. Podemos hacer esto verificando explícitamente qué contenedor está en uso antes de renderizar el componente.
La creación de la capa de abstracción del contenedor funciona bien para los proyectos en su conjunto, pero cuando la elección del contenedor afecta los cambios a nivel de componente , es muy difícil mantener una abstracción limpia. En este caso, hemos perdido un poco de abstracción y tenemos una lógica de bifurcación desordenada en nuestro código. Afortunadamente, estos casos son pocos y distantes entre sí.
¿Cómo manejamos las funciones que faltan?
Mantener la abstracción está muy bien. Nuestro código le dice al contenedor lo que quiere que haga la plataforma, por ejemplo, "ir a pantalla completa". Pero, ¿qué pasa si la plataforma a la que estamos enviando no puede pasar a pantalla completa?
El envoltorio hará todo lo posible para no romperse por completo, pero en última instancia, necesita un diseño que recurra con gracia a una solución funcional, ya sea que el método tenga éxito o no. Tenemos que diseñar a la defensiva.
Digamos que tenemos una sección de resultados que contiene algunos gráficos de barras. A menudo nos gusta mantener los valores del gráfico de barras en cero hasta que los gráficos se desplazan a la vista, momento en el que activamos la animación de las barras a su ancho correcto.
Pero si no tenemos un mecanismo para engancharnos a la posición de desplazamiento, como es el caso de nuestro envoltorio AMP, entonces las barras permanecerán para siempre en cero, lo cual es una experiencia completamente engañosa.
Estamos tratando cada vez más de adoptar un enfoque de mejora progresiva en nuestros diseños. Por ejemplo, podríamos proporcionar un botón que sea visible para todas las plataformas de forma predeterminada, pero que se oculte si el contenedor admite el desplazamiento. De esa forma, si el desplazamiento no activa la animación, el usuario aún puede activar la animación manualmente.
Planes para el futuro
Esperamos desarrollar nuevos envoltorios para plataformas como Apple News y Facebook Instant Articles, así como ofrecer a todas las nuevas plataformas una versión "principal" de nuestro contenido lista para usar.
También esperamos mejorar en la mejora progresiva; triunfar en este campo significa desarrollarse defensivamente. Nunca se puede asumir que todas las plataformas ahora y en el futuro admitirán una interacción determinada, pero un proyecto bien diseñado debería poder transmitir su mensaje central sin caer en el primer obstáculo técnico.
Trabajar dentro de los límites de la envoltura es un cambio de paradigma y se siente como una casa a mitad de camino en términos de la solución a largo plazo . Pero hasta que la industria madure hacia un estándar multiplataforma, los editores se verán obligados a implementar sus propias soluciones, o utilizar herramientas como Distro para la conversión de plataforma a plataforma, o ignorar por completo a secciones enteras de su audiencia.
Todavía es pronto para nosotros, pero hasta ahora hemos tenido un gran éxito en el uso del patrón de envoltorio para crear nuestro contenido una vez y entregarlo a la gran cantidad de plataformas que usa nuestro público ahora.