BBCインタラクティブコンテンツがAMP、アプリ、およびWeb全体でどのように機能するか

公開: 2022-03-10
簡単な要約↬多くの追加の開発オーバーヘッドなしに非常に多くのメディアにコンテンツを公開することは難しい場合があります。 クリスアシュトンは、BBCのビジュアルジャーナリズム部門で彼らがどのように問題に取り組んだかを説明します。

BBCのビジュアルジャーナリズムチームでは、電卓から視覚化の新しいストーリーテリング形式に至るまで、刺激的で魅力的でインタラクティブなコンテンツを制作しています。

各アプリケーションは、それ自体で作成するための固有の課題ですが、ほとんどのプロジェクトを多くの異なる言語で展開する必要があることを考えると、さらに重要です。 私たちのコンテンツは、BBC News and SportsのWebサイトだけでなく、iOSとAndroidの同等のアプリ、およびBBCコンテンツを使用するサードパーティのサイトでも機能する必要があります。

ここで、AMP、Facebook Instant Articles、AppleNewsなどの新しいプラットフォームが増えていることを考えてみてください。 各プラットフォームには、独自の制限と独自の公開メカニズムがあります。 これらすべての環境で機能するインタラクティブコンテンツを作成することは、実際の課題です。 BBCでこの問題にどのように取り組んだかを説明します。

例:CanonicalとAMP

これは、実際に動作するのを見るまでは少し理論的なものなので、例を直接掘り下げてみましょう。

ビジュアルジャーナリズムのコンテンツを含むBBCの記事は次のとおりです。

ビジュアルジャーナリズムのコンテンツを含むBBCニュースページのスクリーンショット
私たちのビジュアルジャーナリズムのコンテンツは、ドナルドトランプのイラストで始まり、iframe内にあります

これは、記事の正規バージョン、つまり、ホームページから記事に移動した場合に取得するデフォルトバージョンです。

ジャンプした後もっと! 以下を読み続けてください↓

それでは、記事のAMPバージョンを見てみましょう。

以前と同じコンテンツを含むBBCNews AMPページのスクリーンショットですが、コンテンツはクリップされ、[もっと見る]ボタンがあります
これは通常の記事と同じコンテンツのように見えますが、AMP用に特別に設計された別のiframeを取り込んでいます

正規バージョンとAMPバージョンは同じように見えますが、実際には、動作が異なる2つの異なるエンドポイントです。

  • 正規バージョンでは、フォームを送信すると、選択した国にスクロールします。
  • AMP iframe内から親ページをスクロールできないため、AMPバージョンではスクロールしません。
  • AMPバージョンでは、ビューポートのサイズとスクロール位置に応じて、[もっと見る]ボタンが付いたトリミングされたiframeが表示されます。 これはAMPの機能です。

この記事の正規バージョンとAMPバージョンに加えて、このプロジェクトはニュースアプリにも出荷されました。ニュースアプリは、独自の複雑さと制限があるさらに別のプラットフォームです。 では、これらすべてのプラットフォームをどのようにサポートするのでしょうか。

ツーリングが重要

コンテンツを最初から作成することはありません。 Nodeを使用して単一のコマンドでボイラープレートプロジェクトを生成するYeomanベースのスキャフォールドがあります。

新しいプロジェクトには、Webpack、SASS、デプロイメント、およびコンポーネント化構造が付属しています。 ハンドルバーテンプレートシステムを使用して、国際化もプロジェクトに組み込まれています。 Tom Maslenは、レスポンシブWebデザインを多言語化するための13のヒントを、彼の投稿で詳しく説明しています。

箱から出して、これは1つのプラットフォーム用にコンパイルするのに非常にうまく機能しますが、複数のプラットフォームをサポートする必要があります。 いくつかのコードを掘り下げてみましょう。

埋め込みとスタンドアロン

ビジュアルジャーナリズムでは、コンテンツをiframe内に出力して、グローバルなスクリプトやスタイルの影響を受けずに、記事に自己完結型の「埋め込み」を行えるようにすることがあります。 この例は、この記事の前半の標準的な例に埋め込まれているドナルドトランプインタラクティブです。

一方、コンテンツを生のHTMLとして出力することもあります。 これを行うのは、ページ全体を制御できる場合、または本当に応答性の高いスクロール操作が必要な場合のみです。 これらをそれぞれ「埋め込み」出力と「スタンドアロン」出力と呼びましょう。

「ロボットがあなたの仕事を引き受けますか?」をどのように構築するか想像してみましょう。 「埋め込み」形式と「スタンドアロン」形式の両方でインタラクティブ。

2つのスクリーンショットを並べて表示します。 1つは、ページに埋め込まれたコンテンツを示しています。もう1つは、それ自体がページと同じコンテンツを表示します。
左側に「埋め込み」を示し、右側に「スタンドアロン」ページとしてのコンテンツを示す不自然な例

コンテンツの両方のバージョンは、コードの大部分を共有しますが、2つのバージョン間でJavaScriptの実装にいくつかの決定的な違いがあります。

たとえば、[自動化のリスクを調べる]ボタンを見てください。 ユーザーが送信ボタンを押すと、結果まで自動的にスクロールされます。

コードの「スタンドアロン」バージョンは次のようになります。

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

ただし、これを「埋め込み」出力として作成している場合は、コンテンツがiframe内にあることがわかっているため、別の方法でコーディングする必要があります。

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

また、アプリケーションを全画面表示にする必要がある場合はどうなりますか? 「スタンドアロン」ページを表示している場合、これは非常に簡単です。

 document.body.className += ' fullscreen';
 .fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } 
「TaptoInteract」オーバーレイが埋め込まれたマップのスクリーンショットと、タップされた後のフルスクリーンモードでのマップのスクリーンショット。
フルスクリーン機能を使用して、モバイルでマップモジュールを最大限に活用しています

「埋め込み」の内部からこれを実行しようとすると、この同じコードのコンテンツは、ビューポートではなく、 iframeの幅と高さにスケーリングされます。

以前と同じマップ例のスクリーンショットですが、フルスクリーンモードにはバグがあります。周囲の記事からのテキストは、本来あるべきではない場所に表示されます。
iframe内から全画面表示にするのは難しい場合があります

…したがって、iframe内でフルスクリーンのスタイルを適用することに加えて、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'; } });

複数のプラットフォームのサポートを開始すると、これは多くのスパゲッティコードに変換される可能性があります。

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

プロジェクト内のすべての意味のあるDOMインタラクションに対してこれと同等のことを行うことを想像してみてください。 身震いが終わったら、リラックスできるお茶を作って読み進めてください。

抽象化が鍵

開発者にコード内でこれらの条件を処理するように強制するのではなく、コンテンツと環境の間に抽象化レイヤーを構築しました。 このレイヤーを「ラッパー」と呼びます。

DOMまたはネイティブブラウザイベントを直接クエリする代わりに、 wrapperモジュールを介してリクエストをプロキシできるようになりました。

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

各プラットフォームには、ラッパーメソッドの共通インターフェイスに準拠した独自のラッパー実装があります。 ラッパーはコンテンツをラップし、複雑さを処理します。

アプリケーションがスタンドアロンのラッパーscrollメソッドを呼び出すときに、ラッパーがホストページのネイティブscrollメソッドを呼び出すことを示すUML図。
スタンドアロンラッパーによる単純な「scrollTo」実装

スタンドアロンラッパーのscrollTo関数の実装は非常に単純で、引数を内部でwindow.scrollToに直接渡します。

次に、iframeに同じ機能を実装する別のラッパーを見てみましょう。

アプリケーションがembedラッパーのscrollメソッドを呼び出すと、embedラッパーが要求されたスクロール位置とiframeのオフセットを組み合わせてから、ホストページでネイティブのscrollメソッドをトリガーすることを示すUML図。
埋め込みラッパーによる高度な「scrollTo」実装

「埋め込み」ラッパーは「スタンドアロン」の例と同じ引数を取りますが、iframeオフセットが考慮されるように値を操作します。 この追加がなければ、ユーザーを完全に意図しない場所にスクロールしていたでしょう。

ラッパーパターン

ラッパーを使用すると、プロジェクト間でよりクリーンで読みやすく、一貫性のあるコードが得られます。 また、ラッパーを段階的に改善してメソッドのパフォーマンスとアクセス性を向上させるため、時間の経過とともにマイクロ最適化が可能になります。 したがって、プロジェクトは多くの開発者の経験から恩恵を受けることができます。

では、ラッパーはどのように見えますか?

ラッパー構造

各ラッパーは基本的に、Handlebarsテンプレート、ラッパーJSファイル、およびラッパー固有のスタイルを示すSASSファイルの3つで構成されます。 さらに、基礎となるスキャフォールディングによって公開されるイベントにフックするビルドタスクがあり、各ラッパーが独自の事前コンパイルとクリーンアップを担当します。

これは、埋め込みラッパーの簡略化されたビューです。

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

基盤となるスキャフォールディングは、メインプロジェクトテンプレートをハンドルバーパーシャルとして公開します。これはラッパーによって消費されます。 たとえば、 templates/wrapper.hbsには次のものが含まれる場合があります。

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

scss/wrapper.scssには、アプリケーションコードがそれ自体を定義する必要のないラッパー固有のスタイルが含まれています。 たとえば、埋め込みラッパーは、iframe内に多くのBBCニュースのスタイルを複製します。

最後に、 js/wrapper.js /wrapper.jsにはラッパーAPIのiframed実装が含まれています。詳細は以下のとおりです。 これは、アプリケーションコードとともにコンパイルされるのではなく、プロジェクトに個別に出荷されます。Webpackビルドプロセスでwrapperにグローバルのフラグを付けます。 つまり、アプリケーションを複数のプラットフォームに配信しますが、コードをコンパイルするのは1回だけです。

ラッパーAPI

ラッパーAPIは、いくつかの主要なブラウザーの相互作用を抽象化します。 最も重要なものは次のとおりです。

scrollTo(int)

アクティブなウィンドウの指定された位置までスクロールします。 ラッパーは、スクロールをトリガーする前に指定された整数を正規化して、ホストページが正しい位置にスクロールされるようにします。

getScrollPosition: int

ユーザーの現在の(正規化された)スクロール位置を返します。 iframeの場合、これは、iframeがビューポートの上部に来るまで、アプリケーションに渡されるスクロール位置が実際にはであることを意味します。 これは非常に便利で、コンポーネントが表示されたときにのみアニメーション化するなどの操作を実行できます。

onScroll(callback)

スクロールイベントへのフックを提供します。 スタンドアロンラッパーでは、これは基本的にネイティブスクロールイベントにフックします。 埋め込みラッパーでは、postMessageを介して渡されるため、スクロールイベントの受信にわずかな遅延が発生します。

viewport: {height: int, width: int}

ビューポートの高さと幅を取得する方法(これは、iframe内からクエリを実行すると非常に異なる方法で実装されるため)。

toggleFullScreen

スタンドアロンモードでは、BBCメニューとフッターを非表示にし、 position: fixedれています。 ニュースアプリでは、何もしません—コンテンツはすでにフルスクリーンです。 複雑なのはiframeです。これは、postMessageを介して調整されたiframeの内側と外側の両方にスタイルを適用することに依存しています。

markPageAsLoaded

コンテンツが読み込まれたことをラッパーに通知します。 これは、コンテンツがニュースアプリで機能するために重要です。ニュースアプリは、コンテンツの準備ができていることをアプリに明示的に通知するまで、ユーザーにコンテンツを表示しようとしません。 また、コンテンツのWebバージョンの読み込みスピナーも削除されます。

ラッパーのリスト

将来的には、Facebook InstantArticlesやAppleNewsなどの大規模なプラットフォーム用に追加のラッパーを作成することを想定しています。 これまでに6つのラッパーを作成しました。

スタンドアロンラッパー

スタンドアロンページに配置する必要があるコンテンツのバージョン。 BBCブランドがバンドルされています。

埋め込みラッパー

コンテンツの管理を維持しているため、記事内に配置したり、BBC以外のサイトにシンジケートしたりするのに安全なコンテンツのiframedバージョン。

AMPラッパー

これは、 amp-iframeとしてAMPページに取り込まれるエンドポイントです。

ニュースアプリラッパー

私たちのコンテンツは、独自のbbcvisualjournalism://プロトコルを呼び出す必要があります。

コアラッパー

HTMLのみが含まれています—プロジェクトのCSSまたはJavaScriptは含まれていません。

JSONラッパー

BBC製品間で共有するためのコンテンツのJSON表現。

プラットフォームまでの配線ラッパー

私たちのコンテンツをBBCサイトに表示するために、ジャーナリストに名前空間のパスを提供します。

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

ジャーナリストはこの「インクルードパス」をCMSに入れ、記事の構造をデータベースに保存します。 すべての製品とサービスは、この公開メカニズムの下流にあります。 各プラットフォームは、必要なコンテンツのフレーバーを選択し、プロキシサーバーにそのコンテンツを要求する責任があります。

そのドナルド・トランプを以前からインタラクティブに取り上げましょう。 ここで、CMSのインクルードパスは次のとおりです。

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

正規の記事ページは、コンテンツの「埋め込み」バージョンが必要であることを認識しているため、インクルードパスに/embedを追加します。

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

…プロキシサーバーにリクエストする前に:

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

一方、AMPページでは、インクルードパスが表示され、 /amp :が追加されます。

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

AMPレンダラーは、コンテンツを参照するAMP HTMLをレンダリングするために少し魔法をかけ、 /ampバージョンを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>

サポートされているすべてのプラットフォームには、独自のバージョンのコンテンツがあります。

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

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

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

...等々

このソリューションは、必要に応じて、より多くのプラットフォームタイプを組み込むように拡張できます。

抽象化は難しい

「一度書けばどこにでもデプロイできる」アーキテクチャを構築することは非常に理想的であり、そうです。 ラッパーアーキテクチャが機能するためには、抽象化内での作業に非常に厳密である必要があります。 これは、「[ここにプラットフォーム名を挿入]で機能させるために、このハッキーなことをする」という誘惑と戦わなければならないことを意味します。 コンテンツが出荷される環境を完全に認識しないようにしたいのですが、これは口で言うほど簡単ではありません。

プラットフォームの機能を抽象的に構成するのは難しい

抽象化アプローチの前は、iframeのマークアップなど、出力のあらゆる側面を完全に制御していました。 アクセシビリティの理由でiframeにtitle属性を追加するなど、プロジェクトごとに何かを微調整する必要がある場合は、マークアップを編集するだけで済みます。

ラッパーマークアップがプロジェクトから分離して存在するようになったので、ラッパーマークアップを構成する唯一の方法は、スキャフォールド自体のフックを公開することです。 クロスプラットフォーム機能の場合、これは比較的簡単に実行できますが、特定のプラットフォームのフックを公開すると、抽象化が崩れます。 1つのラッパーでのみ使用される「iframeタイトル」構成オプションを公開する必要はありません。

プロパティにもっと一般的な名前を付けることができます(例: title )。次に、この値をiframeのtitle属性として使用します。 ただし、何がどこで使用されているかを追跡することが難しくなり始め、構成を抽象化して理解できなくなるリスクがあります。 概して、グローバルに使用されるプロパティのみを設定して、構成を可能な限り無駄のないものに保つようにしています。

コンポーネントの動作は複雑になる可能性があります

ウェブ上では、sharetoolsモジュールは、個別にクリック可能なソーシャルネットワーク共有ボタンを吐き出し、新しいウィンドウで事前入力された共有メッセージを開きます。

TwitterとFacebookのソーシャルメディアアイコンを含むBBCsharetoolsセクションのスクリーンショット。
BBC Visual Journalismシェアツールは、ソーシャルシェアオプションのリストを表示します

ニュースアプリでは、モバイルウェブを介して共有したくありません。 ユーザーが関連するアプリケーション(Twitterなど)をインストールしている場合は、アプリ自体で共有します。 理想的には、ネイティブのiOS / Android共有メニューをユーザーに提示し、事前に入力された共有メッセージでアプリを開く前に、ユーザーに共有オプションを選択させます。 独自のbbcvisualjournalism://プロトコルを呼び出すことで、アプリからネイティブ共有メニューをトリガーできます。

メッセージング、Bluetooth、クリップボードへのコピーなどを介して共有するためのオプションを備えたAndroidの共有メニューのスクリーンショット。
Androidのネイティブ共有メニュー

ただし、この画面は、[結果の共有]セクションで[Twitter]または[Facebook]のどちらをタップしてもトリガーされるため、ユーザーは2回選択する必要があります。 1回目はコンテンツ内で、2回目はネイティブポップアップで。

これは奇妙なユーザージャーニーなので、ニュースアプリから個々の共有アイコンを削除し、代わりに一般的な共有ボタンを表示したいと思います。 コンポーネントをレンダリングする前に、どのラッパーが使用されているかを明示的にチェックすることで、これを行うことができます。

ニュースアプリの共有ボタンのスクリーンショット。これは、次のテキストが表示された1つのボタンです。
ニュースアプリで使用される一般的な共有ボタン

ラッパー抽象化レイヤーの構築はプロジェクト全体でうまく機能しますが、ラッパーの選択がコンポーネントレベルでの変更に影響を与える場合、クリーンな抽象化を維持することは非常に困難です。 この場合、少し抽象化が失われ、コードに厄介なフォークロジックが含まれています。 ありがたいことに、これらのケースはごくわずかであり、その間にあります。

欠落している機能をどのように処理しますか?

抽象化を維持することはすべてうまくいっています。 私たちのコードは、「フルスクリーンにする」など、プラットフォームに何をさせたいかをラッパーに伝えます。 しかし、出荷先のプラットフォームが実際にフルスクリーンにならない場合はどうなるでしょうか。

ラッパーは完全に壊れないように最善を尽くしますが、最終的には、メソッドが成功するかどうかに関係なく、正常に機能するソリューションにフォールバックする設計が必要です。 防御的に設計する必要があります。

いくつかの棒グラフを含む結果セクションがあるとしましょう。 グラフがスクロールされて表示されるまで、棒グラフの値をゼロに保つことがよくあります。その時点で、棒が正しい幅にアニメーション化されます。

ユーザーの面積を全国平均と比較した棒グラフのコレクションのスクリーンショット。各バーの値は、バーの右側にテキストとして表示されます。
私の地域に関連する値を示す棒グラフ

ただし、AMPラッパーの場合のように、スクロール位置にフックするメカニズムがない場合、バーは永久にゼロのままになります。これは、完全に誤解を招く経験です。

以前と同じ棒グラフのスクリーンショットですが、棒には0&#37;が​​あります。幅と各バーの値は0&#37;に固定されています。これは正しくありません。
スクロールイベントが転送されない場合の棒グラフの外観

私たちはますます、私たちの設計にプログレッシブエンハンスメントアプローチを採用しようとしています。 たとえば、デフォルトですべてのプラットフォームで表示されるボタンを提供できますが、ラッパーがスクロールをサポートしている場合は非表示になります。 そうすれば、スクロールがアニメーションのトリガーに失敗した場合でも、ユーザーは手動でアニメーションをトリガーできます。

間違った0&#37;と同じ棒グラフのスクリーンショット棒グラフですが、今回は微妙な灰色のオーバーレイと、ユーザーを「結果の表示」に招待する中央のボタンがあります。
代わりにフォールバックボタンを表示して、クリックするとアニメーションをトリガーすることもできます。

将来の計画

AppleNewsやFacebookInstant Articlesなどのプラットフォーム用の新しいラッパーを開発し、すべての新しいプラットフォームにコンテンツの「コア」バージョンをすぐに提供できるようにしたいと考えています。

また、プログレッシブエンハンスメントが向上することを望んでいます。 この分野で成功するということは、防御的に発展することを意味します。 現在および将来、すべてのプラットフォームが特定のインタラクションをサポートするとは限りませんが、適切に設計されたプロジェクトは、最初の技術的なハードルに陥ることなく、コアメッセージを伝えることができるはずです。

ラッパーの範囲内で作業することは、パラダイムシフトのビットであり、長期的な解決策の観点からは、中途半端な家のように感じます。 しかし、業界がクロスプラットフォーム標準に成熟するまで、パブリッシャーは独自のソリューションを展開するか、プラットフォームからプラットフォームへの変換にDistroなどのツールを使用するか、オーディエンスのセクション全体を完全に無視することを余儀なくされます。

まだ初期の段階ですが、これまでのところ、ラッパーパターンを使用してコンテンツを一度作成し、視聴者が現在使用している無数のプラットフォームに配信することに大きな成功を収めています。