はてな社内で10年間運用されていた広告配信システムを刷新し、ネイティブ広告枠に対応しました

アプリケーションエンジニアのid:yanbeです。2011年にはてなに入社し、以後、はてなブックマークのエンジニアやディレクターなどを経て、最近では、社内で利用する広告入稿システムと、その広告を配信するシステムを開発するチームに所属しています。

はてなでは、Webサービスの収益化の手段の一つとして、Google AdSenseなどのアドネットワークによる広告掲載のほかに、広告主との直接契約や広告代理店を介した広告掲載もおこなっています。

後者の具体的な例としては、はてなブックマーク公式スマートフォンアプリの人気エントリーを開くと、一覧に[PR]表記とともに差し込まれる広告枠*1です。

スマートフォンアプリの他に、はてなブックマークのPC版やスマートフォン版にも、このような広告枠が複数存在します。各プラットフォームに設置されている広告枠の掲載状況を効率的に管理するために、はてな社内では「広告データを入稿するためのシステム」と、「広告を配信するシステム」を、2007年ごろから、つまり10年間ほど運用してきました。

最近、長年の運用にともなって技術的、および運用上の課題が出てきたこれらのシステムを刷新し、その過程でいろいろな技術的な工夫をおこなったので、ご紹介します。

これまでの広告入稿システムと広告配信サーバー

新しいシステムをご紹介するために、従来がどのような状況だったかを解説します。

入稿できる項目

広告入稿システムから広告データとして入稿でき、掲載する広告に直接的に反映できるのは、以下の項目のみでした。

  • リンク先URL

(↓1行テキスト広告の場合)

  • 誘導テキスト

(↓バナー広告の場合)

  • バナー画像
  • バナー画像のalt属性

これは、従来からある画像バナー形式、または1行テキスト形式の広告を配信するためのシステムであることを考えると、ごく自然な仕様といえます。

旧・広告タグ

これまでの広告配信システムでは、広告を掲載したいページに設置する広告タグは以下のようなものでした。

<script type="text/javascript"
  src="http://red.st-hatena.com/ad?cid={広告枠ID}&encode=utf8"
  charset="utf-8"></script>

外部JavaScriptコードは以下のような内容になっています。外部スクリプトがページに読み込まれるのと同時に、広告がページ内に展開されます。

if( !document.adwrite ){
  document.adwrite = function(str, element_id){
    var elm = element_id ? document.getElementById(element_id) : null;
    if( elm ){
      setTimeout(function(){
         elm.innerHTML = str;
      },10);
    }else{
      document.write(str);
    }
  };
}

document.adwrite('<a href=\"http://red.st-hatena.com/go?aid=11323&accessrk=c13351c9fb5b8743&url=http%3A%2F%2Fd.hatena.ne.jp%2Fhatenadiary%2Fsearchdiary%3Fword%3D%25BA%25A3%25BD%25B5%25A4%25CE%25A4%25AA%25C2%25EA\">「今週のお題」は毎週木曜に更新中!</a>');

この広告展開コードは、この広告配信システムが開発された2007年当時に広く利用されていた、各Webブラウザが共通してサポートしていたAPI、および、当時主流だったフィーチャーフォンのブラウザで動作することを念頭に作られたものです。単純なバナー広告または1行テキスト広告という、比較的単純な構造の広告を表示するものであることを差し引いても、今見るとかなり素朴な実装といえます。

このコードは現在も動作するとはいえ、現代的なフロントエンドJavaScriptの観点で見ると、以下の課題があります。

  • 2017年現在のブラウザ環境に適応した最適な実装ではない
    • document.writeを利用する実行パスがあるため、script要素のasync属性やdefer属性によって非同期に処理することが出来ない
    • そのため、ページにこの広告タグを貼り付けると、ブラウザは red.st-hatena.com のレスポンスを同期的に待ち、ページの後続部分のレンダリングをブロックする
  • JavaScriptコード内に固定のHTMLマークアップによる広告データが直接埋め込まれている
    • バナー広告や1行テキスト広告といった、あらかじめ決まった体裁の広告以外には対応しにくい
    • 一覧の表示のためのデータをHTTP API経由でJSON形式でやり取りするような、スマートフォンアプリ上のネイティブ広告枠を実現しにくい
    • 広告データの割合が小さく、広告をページ上に表示するためのコードが大半であっても、レスポンスの高速化のための各種のキャッシュ機構を利用できない
      • 広告配信システムの配信実績の集計の仕組みが、ページが表示されるたびに毎回、外部スクリプトが読み込まれる前提で作られているため

ネイティブ広告枠に求められる入稿要素

本記事の冒頭で述べたような記事一覧に通常の記事と同じ体裁で差し込まれる広告は、ネイティブ広告(枠)と呼ばれるものです。ネイティブ広告枠の掲載に必要な入稿要素は、旧来からあるバナー広告と比べて、多様です。以下のような項目の入稿が必要です。

  • 誘導テキスト
  • 画像
  • 1~2行の概要文
  • 広告主体者
  • 広告主体者のロゴ画像や、スマートフォンアプリのアイコン画像

さらに、はてなブックマークのネイティブ広告枠としては、次のような機能性が求められます。

  • 「123 users」 のような、誘導先ページのURLに対応した、はてなブックマーク数の表記と、コメント一覧ページへのリンク
  • エントリー一覧の各項目の右上のボタンをタップすることで利用できる「あとで読む」機能
  • URLを絞り込んだときに、そのサイトの新着エントリーにリンク
  • 誘導先ページのカテゴリーの表記とそれに応じた配色(CSS)

これらを含む、はてなブックマークのWebサイト上のネイティブ広告枠は、以下のような体裁です。

f:id:yanbe:20170706142355p:plain

このスクリーンショットからも分かるとおり、プラットフォーム(スマートフォン向けやPC向け、アプリ等)によって、ネイティブ広告枠に必要な入稿データは異なります。また、こういったプラットフォームごとのHTMLマークアップごとの違いをまともに扱おうとすると、先に述べたような構造が固定されたHTML片を出力する広告タグでは対応しきれません。

掲載するWebサービス側の工夫

はてなブックマークでは2013年より、PC版トップページに「おすすめブックマーク枠」(現:キュレーション枠)としてネイティブ広告枠を設置しています。当時は、国内ではネイティブ広告枠はまだまだ一般的なものではありませんでした。また、既存の広告配信サーバーや入稿システムの改修は、工数などの面から困難でした。そのため、ネイティブ広告枠として足りない入稿要素は、はてなブックマークの内部でクロールしたエントリーのデータを利用して補っていました。

f:id:yanbe:20170706160729p:plain

しかしこの方法は、本来はブラウザのJavaScriptエンジンによって解釈されることを想定したJavaScriptコードを、はてなブックマークのサーバーサイドで取得し、広告データを取り出した後に、人気エントリーなどの一覧の他の項目と同じ体裁になるよう、足りない表示項目を補完することが必要です。そのため、少々無理があるアーキテクチャにならざるを得ませんでした。

運用の工夫によるカバーの限界

はてなブックマークのネイティブ広告枠の販売が2013年に始まって以降、ネイティブ広告枠への広告掲載の引き合いは徐々に増えていき、広告枠の数や種類も増え、広告データを差し替える頻度も上がっていきました。

その一方で、広告の入稿業務システムは2007年頃の、差し替え頻度が今よりずっと低い現場での広告入稿・管理を前提としたものだったので、大量の広告差し替えの業務をサポートする機能はあまりありませんでした。

暫定的な対策として、はてなブックマークの管理画面で広告の掲載スケジュール設定機能を限定的にサポートするなどの工夫をおこったものの、連携が不十分な複数の管理画面を行ったり来たりする運用はなかなか大変で、2016年あたりには、社内の入稿や掲載確認といった業務の負荷が限界に近づいていました。

そして新システム構築へ

広告の入稿や差し替え業務の効率が改善されれば、より多くの広告掲載依頼をさばくことができるようになり、それによって広告の収益が増える見込みがありました。一方で、既存の広告入稿・配信システムの改修は、コードベースの老朽化から難しい状況でした。そこで、現代的なネイティブ広告枠の要件に対応した新たな広告入稿・配信システムを別途構築し、それを置き換えていくことになりました。

新しい広告配信サーバーの技術的な工夫

新しい広告入稿システムと広告配信サーバーは、広告掲載状況を管理するスタッフの作業負荷を下げ、広告を掲載する先のサービスの開発チームの負担を減らすよう、技術的な工夫をおこなっています。

広告入稿システム側の工夫は別な機会に譲ることにして、本記事では、広告配信サーバー側の振る舞いで旧来のシステムより改善されたポイントをご紹介します。

入稿項目の自由化

ここでは簡単に触れるにとどめますが、広告入稿システム上では、新たな入稿項目を追加するのを容易にしました。これは、将来的なネイティブ広告に求められる体裁の変化に備えたものです。

ネイティブ広告枠に求められる体裁は、設置先のメディアによって様々なので、新たな入稿項目が必要となる場合がときどきあります。

広告タグと広告データのエンドポイントの分離と、複数の広告データの一括取得

Webページ向けのネイティブ広告枠において、1ページ内に複数の広告枠がある場合も、広告タグの外部スクリプトの読み込みが一回で済むようになりました。広告データを分離したことで、外部スクリプトが静的ファイルとなったので、ETagなどのHTTPの標準的なキャッシュ機構が活用できるようになりました。その結果、外部スクリプトを広告配信サーバーから毎回取得する必要がなくなり、2回目以降の広告表示では外部スクリプトに更新がない限り端末のローカルキャッシュを利用できるようになりました。

また広告データの取得に関しても、複数の広告枠への取得リクエストをまとめておこなうようにしたため、1ページあたりの広告の表示に必要な外部リソースの平均読み込み回数が減りました。

これらの工夫により、広告配信システムは以前より少ないマシンリソースで効率的に、かつ高速に広告を配信できるようになりました。

スマートフォンアプリ向け: 広告データ取得JSON APIの提供

はてなブックマークの公式スマートフォンアプリにおいて、人気エントリー一覧などの表示は、スマートフォンアプリがブックマークのAPIエンドポイントからJSON形式の一覧のデータを受け取って、そのデータを基にアプリ上で各項目を組み立てることで実現しています。

この一覧に、他の項目と同等の機能性をもつネイティブ広告枠を挟み込むためには、サービスのサーバーサイドにおいてアプリから記事一覧のリクエストがあったタイミングで、広告配信サーバーから広告データを取得し、スマートフォンアプリへのJSONレスポンスに含める必要があります。

これを実現するには、はてなの各サービスのアプリケーションサーバー内での利用を想定した、広告の掲載に必要なデータ(誘導テキストや誘導先URL、広告の画像部分のURLなど)を、機械的に処理しやすいJSON APIなどで受け取れると望ましいです。

f:id:yanbe:20170711123555p:plain

そのため、まずは各プラットフォームに存在する広告枠への広告データ配信の基準となるJSON APIを作りました。これを基本インタフェースとし、スマートフォンアプリから利用する(a1~a3)他にも、Webページ向けの広告タグ(b1~b3)でも同じJSON APIを利用するアーキテクチャをとりました。こうすることで、広告サーバー側の実装が単純になります。また、広告タグと広告データの分離ができ、ユーザーのブラウザで高速にネイティブ広告を表示するためのHTTPキャッシュを上手く使えるようになります。

Webページ向け: ページの体裁に合わせた広告のレンダリング

前述のとおり、Webページを対象にしたはてなのネイティブ広告枠は、プラットフォームによって体裁が異なります。そのため、広告をWebページ上に展開するときに利用するHTML片は、広告枠ごとに自由に設定できるようになっています。さらに、実際にネイティブ広告枠の設置を担当することになる、はてなの各サービスの開発チーム向けに、実装の負担を減らすことを目的とした広告タグを提供します。

既存の広告タグの問題点

外部の広告配信サーバーから広告を取得し、ページ上に広告掲載するするためのJavaScriptコードとHTML片からなるスニペットを、一般に広告タグと呼びます。一般に利用できる広告タグは、最近のWebアプリケーションのフロントエンド事情を考慮されていないものも多いです。

この状況は、Webアプリケーションのフロントエンド開発者にとって悩みのタネになることがあります。ページの途中に差し込まれる広告タグによってページのレンダリングが遅くなったり、script要素によるインラインJavaScriptのコードと、アプリケーションのbundleされたJavaScriptコードをそれぞれ管理し、それらのロード順も気にする必要性が出てくるためです。

とくにはてなブックマークのネイティブ広告枠の場合、はてなブックマーク数や「あとで読む」機能のような、サービス固有の追加の機能性の実現ために、アプリケーション側の外部スクリプトと協調して動作する必要があります。その際、Webアプリケーション側のbundleされた外部スクリプトと、広告タグから読み込まれる外部スクリプトの間のロード順が問題になることが予想できました。具体的には、各script要素のasync属性が有効になっているとき、アプリケーションの外部スクリプトが読み込まれたタイミングで、広告タグの外部スクリプトが読み込みが完了している保証がないということです。

新しい広告タグの設計方針

こういった背景から、今回開発した新しい広告配信サーバーが提供する広告タグは、 現代的なWebアプリケーションのフロントエンド事情に配慮したものになっています。

具体的には、新しい広告タグは、事前に外部スクリプトの読み込みが完了していなくても正しく動作をします。このため、TypeScriptなどからビルドされ、1つの外部スクリプトとしてバンドルされた、アプリケーションのJavaScriptコードと組み合わせて利用しやすくなっています。

以下で、具体的な広告タグの例をもって説明します。

メインの外部スクリプト読み込み

設置された広告枠の情報を基に、広告配信サーバーに広告データをリクエストし、ページにレンダリングする処理は、適切なHTTPキャッシュが効いた外部スクリプトとして提供されています。

<script
  src="https://ad-hatena.com/js/river.js"
  id="river-js"
  data-river-media-path="/hatena/bookmark_pc"
  async>
</script>

data-river-media-path 属性は、この広告枠がどのメディアに属するかを示しています。広告枠に対する広告データを取得する際に利用されます。上の例では、はてなブックマークのPC版 ( /hatena/bookmark_pc ) であることを示しています。

広告掲載リクエスト
<ins class="river-placement" data-river-placement="{page_category_nth}"></ins>
<script>(riverAds = window.riverAds || []).push(
  function(river){ river.registerPlacement('hotentry_it_4'); }
)</script>

このコード片は、広告を実際に掲載したい位置に挿入するプレースホルダ要素と、そのプレースホルダ要素への広告取得・掲載リクエストです。

広告の掲載をおこないたいタイミング、つまりインプレッションを計測したいタイミングは、アプリケーションのフロントエンドのつくりによって、様々です。そのため、メインの外部スクリプトが読み込まれる前でも、ページの最初のレンダリングが終わった後でも、どのタイミングで広告タグが実行されても正しく動作することが望ましいです。このコード片は、単にページのHTML上に設置してもよく、アプリケーションのJavaScriptから動的にプレースホルダ要素を生成しても問題なく動作します。

自由な広告枠IDの定義

一般的な広告タグにおいて、個々の広告枠を識別するIDは、広告配信システムが発行した無意味な数字であることが多いです。 これはメディア側での掲載先ページとの対応の管理を難しくします。そのため、広告タグを設置するメディアの開発チームにとっては、正しく広告タグが設置出来ているかの確認が難しいという課題がありました。

そこで、掲載プラットフォーム(上の例では、はてなブックマークのPC版 /hatena/bookmark_pc) や、そのメディア内でのページの種類や掲載位置(hotentry_it_4: 人気エントリー ITカテゴリ 4番目)といった情報を使って、アプリケーションのページ構成に応じた広告枠IDを設定出来るようにしました。

これにより、アプリケーションの内部での広告枠の管理が容易になりました。

アプリケーション向けのイベントハンドラの登録

広告データの取得とページ上への掲載が完了した際に、広告タグ固有のJavaScriptイベントが発生するようになっています。この機構により、ネイティブ広告枠に広告が読み込まれ、表示された直後に、任意の処理を専用のイベントハンドラとして記述出来るようになりました。これにより、従来では難しかった、Webアプリケーションのフロントエンド側での追加的なデータ(はてなブックマーク数や、ユーザーごとの「あとで読む」機能のボタンの利用状態など)の読み込みを、アプリケーションのフロントエンドに存在する実装を流用しておこなうことが容易になりました。

(riverAds = window.riverAds || []).push(function(river) {
    river.addEventListener('render:done', function(event) {
        Hatena.Bookmark.fillBookmarkCount(
            event.detail.creative.content_url,
            event.target.querySelector('.users')
        );
    });
    river.addEventListener('render:empty', function(event) {
        event.target.style.dipslay = 'none';
    });
});

このコード例では以下のことをおこなっています。

  • 広告の掲載に成功したとき(render:done イベント)に、その広告の誘導先のcanonical URL(content_url)を基に、アプリケーションのフロントエンドの実装を利用して、ブックマーク数を取得し、広告内の指定の要素に付加する
  • もし掲載すべき広告がなかった場合(render:empty イベント)に、広告用に確保していた領域を畳む

メインの広告スクリプトが読み込まれるまでは、イベントハンドラを登録する処理を専用の配列に一旦キューイングすることで、外部スクリプトのロード順を気にするなことなくイベントハンドラを登録出来るようになっています。

まとめ

以上、はてなのネイティブ広告に関する最近の技術的な取り組みをご紹介しました。入稿側と配信側、両方のシステムの刷新により、変更や拡張が容易になり、配信システムのスケーラビリティも向上したため、以前のシステムでは難しかった、新たな要件のネイティブ広告に対応することがやりやすくなりました。

本件は、広告販売の直接契約が成り立つ規模のメディアを運営する企業における、自社向けの広告入稿システムおよび広告配信サーバーの刷新という、あまりない事例だとは思います。

メディアに掲載する広告枠としてのネイティブ広告枠には、一般的には既存の広告入稿システムや広告配信サーバーを利用することが多いと思います。しかし、ネイティブ広告枠特有の悩みとして、はてなブックマークの例のような、メディアが固有にもつ機能性まで再現しようとすると、技術的な制約からある程度の妥協をせざるを得ない場合があります。多くのメディアに共通の広告タグを提供するアドネットワーク側は、こういった個別のパブリッシャーの事情に対応するのは難しいと思われるので、ある程度は仕方がありません。

一方、今回の事例は、自社のサービスで利用するためのシステムであったため、実装上のペインポイントを押さえた上でアーキテクチャの設計を行うことで、広告枠に関しても機能性を妥協することなく、また、はてなの各サービスの開発チームに、技術的に筋の悪い実装を強いることなく実現できるようになりました。 結果として、将来的にネイティブ広告の要件やトレンドが変わっても対応できる、長く利用できる仕組みが出来たのではないかと考えております。

実のところ、2013年の時点におけるはてなブックマークの最初のネイティブ広告枠は、アプリケーションエンジニアとしては当時2年目で、今より経験が少なかった自分(id:yanbe)が担当しました。当初はトライアル的な広告枠ということで始まったため、工数的な制約もさることながら、アーキテクチャ的にも少々、無理があるものでした。その後、ネイティブ広告の活況も手伝って、機能拡張が繰り返され、関連するコードベースはレガシー化していき、新しいネイティブ広告枠を後任の担当者が苦労して実装する状況になってしまっていました。それを図らずも4年後に、アプリケーションエンジニアとして7年目である自分の手で、望ましいアーキテクチャに置き換える機会に恵まれたので、そういう点でも良かったなと思っています。

求人

はてなでは、将来的な拡張を見据えたよりよい設計を議論しながら、長期の運用に耐えうるWebアプリケーションの設計・開発していく仲間を募集しています。

hatenacorp.jp

*1:詳しくは、「はてなメディアガイド」をご覧ください http://hatenacorp.jp/ads