Cómo funciona el contenido interactivo de la BBC en AMP, aplicaciones y la Web

Publicado: 2022-03-10
Resumen rápido ↬ La publicación de contenido en tantos medios sin muchos gastos adicionales de desarrollo puede ser difícil. Chris Ashton explica cómo han abordado el problema en el departamento de Periodismo Visual de la BBC.

En 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:

Captura de pantalla de la página de noticias de la BBC con contenido de periodismo visual
Nuestro contenido de Periodismo Visual comienza con la ilustración de Donald Trump y está dentro de un iframe

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.

¡Más después del salto! Continúe leyendo a continuación ↓

Ahora veamos la versión AMP del artículo:

Captura de pantalla de la página AMP de BBC News que contiene el mismo contenido que antes, pero el contenido está recortado y tiene un botón Mostrar más
Parece el mismo contenido que el artículo normal, pero muestra un iframe diferente diseñado específicamente para AMP.

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".

Dos capturas de pantalla una al lado de la otra. Uno muestra contenido incrustado en una página; el otro muestra el mismo contenido que una página por derecho propio.
Ejemplo artificial que muestra una 'inserción' a la izquierda, frente al contenido como una página 'independiente' a la derecha

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; } 
Captura de pantalla del mapa incrustado con la superposición "Tocar para interactuar", seguida de una captura de pantalla del mapa en modo de pantalla completa después de haberlo tocado.
Usamos con éxito la funcionalidad de pantalla completa para aprovechar al máximo nuestro módulo de mapas en dispositivos móviles

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:

Captura de pantalla del ejemplo de mapa como antes, pero el modo de pantalla completa tiene errores. El texto del artículo que lo rodea es visible donde no debería estar.
Puede ser difícil ir a pantalla completa desde dentro de un iframe

…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.

Diagrama UML que muestra que cuando nuestra aplicación llama al método de desplazamiento del contenedor independiente, el contenedor llama al método de desplazamiento nativo en la página host.
Implementación simple de 'scrollTo' por el contenedor independiente

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:

Diagrama UML que muestra que cuando nuestra aplicación llama al método de desplazamiento del envoltorio de inserción, el envoltorio de inserción combina la posición de desplazamiento solicitada con el desplazamiento del iframe antes de activar el método de desplazamiento nativo en la página host.
Implementación avanzada de 'scrollTo' por el contenedor incrustado

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.

Captura de pantalla de la sección de herramientas para compartir de la BBC que contiene íconos de redes sociales de Twitter y Facebook.
Las herramientas para compartir BBC Visual Journalism presentan una lista de opciones para compartir en redes sociales

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:// .

Captura de pantalla del menú Compartir en Android con opciones para compartir a través de Mensajería, Bluetooth, Copiar al portapapeles, etc.
Menú de compartir nativo en Android

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.

Captura de pantalla del botón para compartir de la aplicación de noticias. Este es un solo botón con el siguiente texto: 'Comparte cómo lo hiciste'.
Botón de compartir genérico utilizado en la aplicación de noticias

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.

Captura de pantalla de una colección de gráficos de barras que comparan el área del usuario con los promedios nacionales. Cada barra tiene su valor mostrado como texto a la derecha de la barra.
Gráfico de barras que muestra valores relevantes para mi área

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.

La misma captura de pantalla de los gráficos de barras que antes, pero las barras tienen 0&#37; ancho y los valores de cada barra se fijan en 0&#37;. Esto es incorrecto.
Cómo podría verse el gráfico de barras si no se reenvían los eventos de desplazamiento

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.

La misma captura de pantalla de los gráficos de barras que el 0&#37; gráficos de barras, pero esta vez con una superposición gris sutil y un botón centrado que invita al usuario a 'Ver resultados'.
En su lugar, podríamos mostrar un botón de respaldo, que activa la animación al hacer clic.

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.