Como o conteúdo interativo da BBC funciona em AMP, aplicativos e na Web

Publicados: 2022-03-10
Resumo rápido ↬ A publicação de conteúdo em tantas mídias sem muita sobrecarga de desenvolvimento extra pode ser difícil. Chris Ashton explica como eles abordaram o problema no departamento de jornalismo visual da BBC.

Na equipe de Jornalismo Visual da BBC, produzimos conteúdo visual, envolvente e interativo empolgante, que vai de calculadoras a visualizações de novos formatos de narrativa.

Cada aplicativo é um desafio único para produzir por si só, mas ainda mais quando você considera que temos que implantar a maioria dos projetos em muitos idiomas diferentes. Nosso conteúdo deve funcionar não apenas nos sites de notícias e esportes da BBC, mas em seus aplicativos equivalentes no iOS e Android, bem como em sites de terceiros que consomem conteúdo da BBC.

Agora considere que há uma variedade crescente de novas plataformas , como AMP, Facebook Instant Articles e Apple News. Cada plataforma tem suas próprias limitações e mecanismo de publicação proprietário. Criar conteúdo interativo que funcione em todos esses ambientes é um verdadeiro desafio. Vou descrever como abordamos o problema na BBC.

Exemplo: canônico vs. AMP

Isso tudo é um pouco teórico até você vê-lo em ação, então vamos nos aprofundar em um exemplo.

Aqui está um artigo da BBC contendo conteúdo de Jornalismo Visual:

Captura de tela da página da BBC News contendo conteúdo de jornalismo visual
Nosso conteúdo de Jornalismo Visual começa com a ilustração de Donald Trump e está dentro de um iframe

Esta é a versão canônica do artigo, ou seja, a versão padrão, que você obterá se navegar para o artigo na página inicial.

Mais depois do salto! Continue lendo abaixo ↓

Agora vamos ver a versão AMP do artigo:

Captura de tela da página AMP da BBC News contendo o mesmo conteúdo de antes, mas o conteúdo é recortado e tem um botão Mostrar mais
Parece o mesmo conteúdo do artigo normal, mas está usando um iframe diferente projetado especificamente para AMP

Embora as versões canônica e AMP pareçam iguais, na verdade são dois endpoints diferentes com comportamento diferente:

  • A versão canônica rola para o país escolhido quando você envia o formulário.
  • A versão AMP não rola você, pois você não pode rolar a página pai de dentro de um iframe AMP.
  • A versão AMP mostra um iframe cortado com um botão 'Mostrar mais', dependendo do tamanho da janela de visualização e da posição de rolagem. Este é um recurso do AMP.

Além das versões canônica e AMP deste artigo, este projeto também foi enviado para o News App, que é mais uma plataforma com seus próprios meandros e limitações. Então , como suportamos todas essas plataformas?

Ferramental é a chave

Não construímos nosso conteúdo do zero. Temos um scaffold baseado em Yeoman que usa o Node para gerar um projeto clichê com um único comando.

Novos projetos vêm com Webpack, SASS, implantação e uma estrutura de componentização pronta para uso. A internacionalização também está incorporada em nossos projetos, usando um sistema de templates Handlebars. Tom Maslen escreve sobre isso em detalhes em seu post, 13 dicas para tornar o web design responsivo multilíngue.

Fora da caixa, isso funciona muito bem para compilar para uma plataforma, mas precisamos oferecer suporte a várias plataformas . Vamos mergulhar em algum código.

Incorporar vs. Independente

No Jornalismo Visual, às vezes produzimos nosso conteúdo dentro de um iframe para que ele possa ser uma “incorporação” independente em um artigo, não afetado pelo script e estilo globais. Um exemplo disso é o Donald Trump interativo incorporado no exemplo canônico anteriormente neste artigo.

Por outro lado, às vezes produzimos nosso conteúdo como HTML bruto. Só fazemos isso quando temos controle sobre toda a página ou se precisarmos de uma interação de rolagem realmente responsiva. Vamos chamá-las de nossas saídas “incorporadas” e “independentes”, respectivamente.

Vamos imaginar como poderíamos construir o “Será que um robô vai tirar o seu emprego?” interativo nos formatos “incorporado” e “autônomo”.

Duas capturas de tela lado a lado. Um mostra o conteúdo incorporado em uma página; o outro mostra o mesmo conteúdo de uma página por direito próprio.
Exemplo artificial mostrando uma 'incorporação' à esquerda versus o conteúdo como uma página 'independente' à direita

Ambas as versões do conteúdo compartilhariam a grande maioria de seu código, mas haveria algumas diferenças cruciais na implementação do JavaScript entre as duas versões.

Por exemplo, veja o botão 'Descobrir meu risco de automação'. Quando o usuário clica no botão enviar, ele deve ser rolado automaticamente para seus resultados.

A versão “independente” do código pode ser assim:

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

Mas se você estivesse construindo isso como saída “incorporada”, você sabe que seu conteúdo está dentro de um iframe, então precisaria codificá-lo de forma 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); } });

Além disso, e se nosso aplicativo precisar ficar em tela cheia? Isso é bastante fácil se você estiver em uma página “independente”:

 document.body.className += ' fullscreen';
 .fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } 
Captura de tela da incorporação do mapa com a sobreposição 'Toque para interagir', seguida de uma captura de tela do mapa no modo de tela inteira após o toque.
Usamos com sucesso a funcionalidade de tela cheia para aproveitar ao máximo nosso módulo de mapa no celular

Se tentássemos fazer isso de dentro de uma “incorporação”, esse mesmo código teria o dimensionamento do conteúdo para a largura e a altura do iframe , em vez da janela de visualização:

Captura de tela do exemplo de mapa como antes, mas o modo de tela cheia está com erros. O texto do artigo ao redor fica visível onde não deveria.
Pode ser difícil ir em tela cheia de dentro de um iframe

…então, além de aplicar o estilo de tela inteira dentro do iframe, temos que enviar uma mensagem para a página host para aplicar o estilo ao próprio 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'; } });

Isso pode se traduzir em muito código espaguete quando você começar a oferecer suporte a várias plataformas:

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

Imagine fazer um equivalente disso para cada interação significativa do DOM em seu projeto. Quando terminar de estremecer, prepare uma xícara de chá relaxante e continue lendo.

A abstração é a chave

Em vez de forçar nossos desenvolvedores a lidar com essas condicionais dentro de seu código, construímos uma camada de abstração entre seu conteúdo e o ambiente. Chamamos essa camada de 'embrulho'.

Em vez de consultar diretamente os eventos do DOM ou do navegador nativo, agora podemos fazer proxy de nossa solicitação por meio do módulo wrapper .

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

Cada plataforma tem sua própria implementação de wrapper em conformidade com uma interface comum de métodos de wrapper. O wrapper envolve nosso conteúdo e lida com a complexidade para nós.

Diagrama UML mostrando que quando nosso aplicativo chama o método de rolagem do wrapper independente, o wrapper chama o método de rolagem nativo na página do host.
Implementação simples de 'scrollTo' pelo wrapper autônomo

A implementação do wrapper autônomo da função scrollTo é muito simples, passando nosso argumento diretamente para window.scrollTo sob o capô.

Agora vamos ver um wrapper separado implementando a mesma funcionalidade para o iframe:

Diagrama UML mostrando que quando nosso aplicativo chama o método de rolagem do wrapper incorporado, o wrapper incorporado combina a posição de rolagem solicitada com o deslocamento do iframe antes de acionar o método de rolagem nativo na página do host.
Implementação avançada de 'scrollTo' pelo wrapper de incorporação

O wrapper “embed” usa o mesmo argumento do exemplo “standalone”, mas manipula o valor para que o deslocamento do iframe seja levado em consideração. Sem essa adição, teríamos rolado nosso usuário para algum lugar completamente não intencional.

O padrão do invólucro

O uso de wrappers resulta em um código mais limpo, mais legível e consistente entre projetos. Também permite micro-otimizações ao longo do tempo, pois fazemos melhorias incrementais nos wrappers para tornar seus métodos mais eficientes e acessíveis. Seu projeto pode, portanto, se beneficiar da experiência de muitos desenvolvedores.

Então, como é um invólucro?

Estrutura do invólucro

Cada wrapper compreende essencialmente três coisas: um modelo Handlebars, um arquivo JS de wrapper e um arquivo SASS que denota um estilo específico do wrapper. Além disso, existem tarefas de compilação que se conectam a eventos expostos pelo scaffolding subjacente para que cada wrapper seja responsável por sua própria pré-compilação e limpeza.

Esta é uma visão simplificada do wrapper de incorporação:

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

Nosso scaffolding subjacente expõe seu modelo de projeto principal como um Handlebars parcial, que é consumido pelo wrapper. Por exemplo, templates/wrapper.hbs pode conter:

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

scss/wrapper.scss contém um estilo específico de wrapper que seu código de aplicativo não precisa definir. O wrapper de incorporação, por exemplo, replica muito do estilo da BBC News dentro do iframe.

Por fim, js/wrapper.js contém a implementação de iframe da API do wrapper, detalhada abaixo. Ele é enviado separadamente para o projeto, em vez de compilado com o código do aplicativo — sinalizamos o wrapper como global em nosso processo de compilação do Webpack. Isso significa que, embora entreguemos nosso aplicativo para várias plataformas, compilamos o código apenas uma vez.

API do wrapper

A API do wrapper abstrai várias interações principais do navegador. Aqui estão os mais importantes:

scrollTo(int)

Rola para a posição especificada na janela ativa. O wrapper normalizará o inteiro fornecido antes de acionar a rolagem para que a página do host seja rolada para a posição correta.

getScrollPosition: int

Retorna a posição de rolagem atual (normalizada) do usuário. No caso do iframe, isso significa que a posição de rolagem passada para seu aplicativo é realmente negativa até que o iframe esteja na parte superior da viewport. Isso é super útil e nos permite fazer coisas como animar um componente apenas quando ele estiver visível.

onScroll(callback)

Fornece um gancho para o evento de rolagem. No wrapper autônomo, isso é essencialmente conectar-se ao evento de rolagem nativo. No wrapper de incorporação, haverá um pequeno atraso no recebimento do evento de rolagem, pois ele é transmitido via postMessage.

viewport: {height: int, width: int}

Um método para recuperar a altura e a largura da janela de visualização (já que isso é implementado de maneira muito diferente quando consultado de dentro de um iframe).

toggleFullScreen

No modo autônomo, ocultamos o menu e o rodapé da BBC e definimos uma position: fixed em nosso conteúdo. No News App, não fazemos nada — o conteúdo já está em tela cheia. O complicado é o iframe, que se baseia na aplicação de estilos dentro e fora do iframe, coordenados via postMessage.

markPageAsLoaded

Diga ao wrapper que seu conteúdo foi carregado. Isso é crucial para que nosso conteúdo funcione no aplicativo de notícias, que não tentará exibir nosso conteúdo para o usuário até que informemos explicitamente ao aplicativo que nosso conteúdo está pronto. Ele também remove o spinner de carregamento nas versões da web do nosso conteúdo.

Lista de invólucros

No futuro, prevemos criar wrappers adicionais para grandes plataformas, como Facebook Instant Articles e Apple News. Criamos seis wrappers até o momento:

Invólucro Autônomo

A versão do nosso conteúdo que deve ir em páginas independentes. Vem empacotado com a marca BBC.

Incorporar wrapper

A versão em iframe do nosso conteúdo, que é segura para ficar dentro de artigos ou para sindicar em sites que não sejam da BBC, uma vez que mantemos o controle sobre o conteúdo.

Wrapper AMP

Esse é o endpoint que é inserido como um amp-iframe nas páginas AMP.

Wrapper de aplicativo de notícias

Nosso conteúdo deve fazer chamadas para um protocolo proprietário bbcvisualjournalism:// .

Envoltório do núcleo

Contém apenas o HTML — nenhum CSS ou JavaScript do nosso projeto.

Embrulho JSON

Uma representação JSON do nosso conteúdo, para compartilhamento nos produtos da BBC.

Envolvedores de fiação até as plataformas

Para que nosso conteúdo apareça no site da BBC, fornecemos aos jornalistas um caminho com namespace:

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

O jornalista coloca esse “caminho de inclusão” no CMS, que salva a estrutura do artigo no banco de dados. Todos os produtos e serviços ficam abaixo desse mecanismo de publicação. Cada plataforma é responsável por escolher o tipo de conteúdo que deseja e solicitar esse conteúdo de um servidor proxy.

Vamos usar o Donald Trump interativo de antes. Aqui, o caminho de inclusão no CMS é:

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

A página do artigo canônico sabe que quer a versão “incorporada” do conteúdo, então anexa /embed ao caminho de inclusão:

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

…antes de solicitá-lo do servidor proxy:

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

A página AMP, por outro lado, vê o caminho de inclusão e anexa /amp :

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

O renderizador AMP faz um pouco de mágica para renderizar alguns HTMLs AMP que fazem referência ao nosso conteúdo, extraindo a versão /amp como um 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 suportada tem sua própria versão do conteúdo:

 /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 assim por diante

Essa solução pode ser dimensionada para incorporar mais tipos de plataforma à medida que surgem.

A abstração é difícil

Construir uma arquitetura “escreva uma vez, implemente em qualquer lugar” parece bastante idealista, e é. Para que a arquitetura do wrapper funcione, temos que ser muito rigorosos em trabalhar dentro da abstração. Isso significa que temos que lutar contra a tentação de “fazer essa coisa hacky para que funcione em [insira o nome da plataforma aqui]”. Queremos que nosso conteúdo seja completamente inconsciente do ambiente em que é enviado - mas isso é mais fácil dizer do que fazer.

Recursos da plataforma são difíceis de configurar abstratamente

Antes de nossa abordagem de abstração, tínhamos controle total sobre todos os aspectos de nossa saída, incluindo, por exemplo, a marcação de nosso iframe. Se precisássemos ajustar qualquer coisa por projeto, como adicionar um atributo de title ao iframe por motivos de acessibilidade, poderíamos apenas editar a marcação.

Agora que a marcação do wrapper existe isoladamente do projeto, a única maneira de configurá-la seria expor um gancho no próprio scaffold. Podemos fazer isso com relativa facilidade para recursos de plataforma cruzada, mas expor ganchos para plataformas específicas quebra a abstração. Nós realmente não queremos expor uma opção de configuração 'iframe title' que é usada apenas por um wrapper.

Poderíamos nomear a propriedade de forma mais genérica, por exemplo, title , e então usar esse valor como o atributo title do iframe. No entanto, começa a ficar difícil acompanhar o que é usado onde, e corremos o risco de abstrair nossa configuração a ponto de não mais entendê-la. Em geral, tentamos manter nossa configuração o mais enxuta possível, apenas definindo propriedades que tenham uso global.

O comportamento do componente pode ser complexo

Na web, nosso módulo sharetools exibe botões de compartilhamento de rede social que podem ser clicados individualmente e abrem uma mensagem de compartilhamento pré-preenchida em uma nova janela.

Captura de tela da seção sharetools da BBC contendo ícones de mídia social do Twitter e do Facebook.
As ferramentas de compartilhamento do BBC Visual Journalism apresentam uma lista de opções de compartilhamento social

No aplicativo de notícias, não queremos compartilhar pela web móvel. Se o usuário tiver o aplicativo relevante instalado (por exemplo, Twitter), queremos compartilhar no próprio aplicativo. Idealmente, queremos apresentar ao usuário o menu de compartilhamento nativo do iOS/Android e permitir que ele escolha sua opção de compartilhamento antes de abrirmos o aplicativo para ele com uma mensagem de compartilhamento pré-preenchida. Podemos acionar o menu de compartilhamento nativo do aplicativo fazendo uma chamada para o protocolo proprietário bbcvisualjournalism:// .

Captura de tela do menu de compartilhamento no Android com opções para compartilhamento via Mensagens, Bluetooth, Copiar para a área de transferência e assim por diante.
Menu de compartilhamento nativo no Android

No entanto, esta tela será acionada se você tocar em 'Twitter' ou 'Facebook' na seção 'Compartilhe seus resultados', então o usuário acaba tendo que fazer sua escolha duas vezes; a primeira vez dentro do nosso conteúdo, e uma segunda vez no pop-up nativo.

Essa é uma jornada do usuário estranha, então queremos remover os ícones de compartilhamento individuais do aplicativo Notícias e mostrar um botão de compartilhamento genérico. Podemos fazer isso verificando explicitamente qual wrapper está em uso antes de renderizar o componente.

Captura de tela do botão de compartilhamento do aplicativo de notícias. Este é um único botão com o seguinte texto: 'Compartilhe como você fez'.
Botão de compartilhamento genérico usado no aplicativo de notícias

Construir a camada de abstração do wrapper funciona bem para projetos como um todo, mas quando a escolha do wrapper afeta as mudanças no nível do componente , é muito difícil manter uma abstração limpa. Nesse caso, perdemos um pouco de abstração e temos uma lógica de bifurcação confusa em nosso código. Felizmente, esses casos são poucos e distantes entre si.

Como lidamos com recursos ausentes?

Manter a abstração é muito bom. Nosso código diz ao wrapper o que ele quer que a plataforma faça, por exemplo, “ir para tela cheia”. Mas e se a plataforma para a qual estamos enviando não puder ser exibida em tela cheia?

O wrapper fará o possível para não quebrar completamente, mas, em última análise, você precisa de um design que retorne graciosamente a uma solução de trabalho, independentemente de o método ter sucesso ou não. Temos que projetar defensivamente.

Digamos que temos uma seção de resultados contendo alguns gráficos de barras. Geralmente, gostamos de manter os valores do gráfico de barras em zero até que os gráficos sejam rolados para a visualização, momento em que acionamos as barras animando para a largura correta.

Captura de tela de uma coleção de gráficos de barras comparando a área do usuário com as médias nacionais. Cada barra tem seu valor exibido como texto à direita da barra.
Gráfico de barras mostrando valores relevantes para minha área

Mas se não tivermos nenhum mecanismo para conectar na posição de rolagem - como é o caso do nosso wrapper AMP - as barras permanecerão para sempre em zero, o que é uma experiência totalmente enganosa.

Mesma captura de tela dos gráficos de barras de antes, mas as barras têm 0&#37; largura e os valores de cada barra são fixados em 0&#37;. Isso está incorreto.
Como o gráfico de barras ficaria se os eventos de rolagem não fossem encaminhados

Estamos cada vez mais tentando adotar uma abordagem de aprimoramento progressivo em nossos projetos. Por exemplo, poderíamos fornecer um botão que ficará visível para todas as plataformas por padrão, mas que ficará oculto se o wrapper suportar rolagem. Dessa forma, se a rolagem não acionar a animação, o usuário ainda poderá acionar a animação manualmente.

Mesma captura de tela de gráficos de barras que o 0&#37; incorreto gráficos de barras, mas desta vez com uma sobreposição cinza sutil e um botão centralizado convidando o usuário a 'Visualizar resultados'.
Poderíamos exibir um botão de fallback, que aciona a animação ao clicar.

Planos para o futuro

Esperamos desenvolver novos wrappers para plataformas como Apple News e Facebook Instant Articles, bem como oferecer a todas as novas plataformas uma versão 'principal' do nosso conteúdo pronto para uso.

Também esperamos melhorar o aprimoramento progressivo; ter sucesso neste campo significa desenvolver defensivamente. Você nunca pode supor que todas as plataformas agora e no futuro suportarão uma determinada interação, mas um projeto bem projetado deve ser capaz de transmitir sua mensagem principal sem cair no primeiro obstáculo técnico.

Trabalhar dentro dos limites do wrapper é um pouco uma mudança de paradigma e parece uma casa no meio do caminho em termos de solução de longo prazo . Mas até que a indústria amadureça em um padrão de plataforma cruzada, os editores serão forçados a lançar suas próprias soluções ou usar ferramentas como Distro para conversão de plataforma para plataforma ou ignorar completamente seções inteiras de seu público.

Ainda é cedo para nós, mas até agora tivemos grande sucesso ao usar o padrão wrapper para criar nosso conteúdo uma vez e entregá-lo às inúmeras plataformas que nosso público está usando agora.