Infinite Scroll(無限スクロール)ページでPrebid.jsを使って広告を出してみる

はじめに

TechDiv アドテクノロジーサービスチームの石鍋です。
前回の記事(Prebid.jsとGoogleタグでHeaderBiddingライフ)に引き続き、prebidjs関連の記事を書いていきたいと思います。

やりたいこと

Infinite Scrollページでコンテンツ追加(広告もセットで追加)時にHeaderBiddingと同様、GAMリクエスト前にprebid機能を用いてオークションを実施させ、広告を表示させる!

Infinite Scrollって?

ページ下部に近づくとコンテンツが追加され、永久にスクロールができるページのことを言います。
特にSP用サイトやアプリでよく見かける手法ですね。

実装サンプル

Infinite Scrollを想定したサンプルであるため、以下のサンプルはページ下部に近づいたタイミングでの自動コンテンツ追加は行わず、moreContentボタンを押下することで、コンテンツ追加時のスクリプトを発火させる簡単な仕組みとしています。

以下のサンプルはPrebidの公式サイトのクイックスタートページGoogleアドマネージャのヘルプページを組み合わせて作成したものとなります。
prebid or googletagのバージョンアップ等で下記サンプルが動作しなくなる可能性がありますが、ご了承ください。
※検証時のprebid.jsのバージョンはv2.31.0

<html>
  <head>
    <link rel="icon" type="image/png" href="/favicon.png">
    <meta charset="utf-8"></meta>
    <script async src="//www.googletagservices.com/tag/js/gpt.js"></script>
    <script async src="//acdn.adnxs.com/prebid/not-for-prod/1/prebid.js"></script>
    <script>
            var sizes = [
                [300, 250]
            ];
            var PREBID_TIMEOUT = 1000;
            var FAILSAFE_TIMEOUT = 3000;

            var adUnits = [{
                code: '/19968336/header-bid-tag-1',
                mediaTypes: {
                    banner: {
                        sizes: sizes
                    }
                },
                bids: [{
                    bidder: 'appnexus',
                    params: {
                        placementId: 13144370
                    }
                }]
            }];

            // ======== DO NOT EDIT BELOW THIS LINE =========== //
            var googletag = googletag || {};
            googletag.cmd = googletag.cmd || [];
            googletag.cmd.push(function() {
                googletag.pubads().disableInitialLoad();
            });

            var pbjs = pbjs || {};
            pbjs.que = pbjs.que || [];

            pbjs.que.push(function() {
                pbjs.addAdUnits(adUnits);
                pbjs.requestBids({
                    bidsBackHandler: initAdserver,
                    timeout: PREBID_TIMEOUT
                });
            });

            function initAdserver() {
                if (pbjs.initAdserverSet) return;
                pbjs.initAdserverSet = true;
                googletag.cmd.push(function() {
                    pbjs.setTargetingForGPTAsync && pbjs.setTargetingForGPTAsync();
                    googletag.pubads().refresh();
                });
            }
            
            // in case PBJS doesn't load
            setTimeout(function() {
                initAdserver();
            }, FAILSAFE_TIMEOUT);

            googletag.cmd.push(function() {
                googletag.defineSlot('/19968336/header-bid-tag-1', sizes, 'div-1')
                        .addService(googletag.pubads());
                googletag.pubads().enableSingleRequest();
                googletag.enableServices();
            });
        </script>
  </head>
  <body>
    <input id="moreContent" type="button" value="moreContent"></input>
    <h2>Basic Prebid.js Example</h2>
    <h5>Div-1</h5>
    <div id='div-1'>
      <script type='text/javascript'>
                googletag.cmd.push(function() {
                    googletag.display('div-1');
                });
            </script>
    </div>

    <script>
var nextOptDivId = 2;
function generateNextOptDivId(divid) {
  var id = nextOptDivId++;
  return divid + id;
}

document.getElementById('moreContent').addEventListener('click', function() {
  var newOptDivId = generateNextOptDivId('div-');
  // コンテンツ追加処理
  addContent(newOptDivId);

  var newAdUnits = [{
      code: newOptDivId,
      mediaTypes: {
          banner: {
              sizes: sizes
          }
      },
      bids: [{
          bidder: 'appnexus',
          params: {
              placementId: 13144370
          }
      }]
  }];
  googletag.cmd.push(function() {
    // ⅰ)新しいdiv-idでdefineSlotを再実施
    var slot = googletag.defineSlot('/19968336/header-bid-tag-1', sizes, newOptDivId).addService(googletag.pubads());
    
    pbjs.que.push(function() {
      var removeCodeList = [];
      // ⅱ)過去の情報を削除しないと過去のオークションも再度実施されることになるため、
      //  削除処理 開始
      pbjs.adUnits.forEach(function(val) {
        removeCodeList.push(val.code);
      });
      removeCodeList.forEach(function(val) {
        pbjs.removeAdUnit(val);
      });
      //  削除処理 終了

      pbjs.addAdUnits(newAdUnits);
      pbjs.requestBids({
        timeout: PREBID_TIMEOUT,
        bidsBackHandler: function() {
          // ⅲ)ターゲティング設定は新しいopt-divのみ実施
          pbjs.setTargetingForGPTAsync([newOptDivId]);
          // ⅳ)広告リフレッシュも新しくdefineSlotしたslotのみにする
          googletag.pubads().refresh([slot]);
        }
      });
    });
  });
});
function addContent(slotName) {
     // 仮のコンテンツ 1 を作成して追加
     var h1=document.createElement("H2")
     var text1=document.createTextNode("仮の動的コンテンツ 1");
     h1.appendChild(text1);
     document.body.appendChild(h1);

     // 仮のコンテンツ 2 を作成して追加
     var h2=document.createElement("H2")
     var text2=document.createTextNode("仮の動的コンテンツ 2");
     h2.appendChild(text2);
     document.body.appendChild(h2);

     // スロット用の div を作成
     var slotDiv = document.createElement('div');
     slotDiv.id = slotName;
     document.body.appendChild(slotDiv);
}
</script>
  </body>
</html>

サンプルコード説明

headタグ内にあるscriptについてはprebid公式ページから持ってきたサンプルなので説明は割愛します。
上記サンプルコードのコメントアウトのⅰ)~ⅳ)について説明していきます。

    // ⅰ)新しいdiv-idでdefineSlotを再実施
    var slot = googletag.defineSlot('/19968336/header-bid-tag-1', sizes, newOptDivId).addService(googletag.pubads());

新しいopt-divでdivを新規に作成し、そのdivとgoogletagの紐付けを行う。defineSlotの第一引数のadUnitPathは基本的(※)に変更しない。
※:Infinite Scrollの広告をどのように出すか次第では、adUnitPathも変更する手段は有りだと思います。今回の記事では一つのadUnitPathでInfinite Scrollを実施しているため、変更しない実装としています。

      // ⅱ)過去の情報を削除しないと過去のオークションも再度実施されることになるため、
      //  削除処理 開始
      pbjs.adUnits.forEach(function(val) {
        removeCodeList.push(val.code);
      });
      removeCodeList.forEach(function(val) {
        pbjs.removeAdUnit(val);
      });
      //  削除処理 終了

pbjs.addAdUnits(xx)で追加したものを削除せずにpbjs.requestBids()を走らせると、過去のadUnitのオークションも実施されることになるため、削除しておく必要があります。

          // ⅲ)ターゲティング設定は新しいopt-divのみ実施
          pbjs.setTargetingForGPTAsync([newOptDivId]);

新規に作成した opt-div のみとしているが、ⅱ)で過去データを削除しているため、引数なしでも問題ないと思います。(引数未設定は未検証)

          // ⅳ)広告リフレッシュも新しくdefineSlotしたslotのみにする
          googletag.pubads().refresh([slot]);

新規に作成したslotのみを対象とします。引数なしとした場合、ページ内のgoogleタグからの広告が全て差し替わることになるので、slot指定は必須となります。

注意事項

サンプルコードを動かしてもらえばわかるかと思いますが、
moreContentボタン押下の
 1回目は正常に広告が表示
 2回目は自社広告が表示
 3回目となると広告が出なくなる
という流れになるかと思います。
この動きに関しては、 googleの仕様で同一ページx同一ラインアイテムx同一クリエイティブは選択しないようにロジックで制御しているためだと思われます。(同時掲載排除機能)
この仕様はInfinite Scroll×HeaderBiddingでは致命的な問題で、
例えば、HeaderBiddingで1つ or 2つなどの接続してるSSPが少ない状況、かつCPMも振れ幅も少ないケースにおいて、この同時掲載排除機能により、数回のオークションしか有効にならないことになります。
現実的ではありませんが、オークションごとにCPMが変動するのであれば、ラインアイテムxクリエイティブは異なるものが選択されることになるので問題なく動作します。

prebidでオークションを実施する上限を設け、その上限と同じ数のクリエイティブをラインアイテムに紐付ける、などの工夫をすることで多少の改善はできるかと思いますが、それはそれでGAMの設定が大変だったり、Infinite Scrollでprebid実現している意味が薄れるので・・。

まとめ

今回はInfinite Scrollxprebidjsで広告表示をテーマに記事を作成しましたが、思いのほかInfinite Scrollとprebidjsの相性が良くないことがわかりました。(もはやHeader Biddingではないですしね・・)
とは言え、こういう仕組みは実際にやってみないとわからないことも多いので、良い勉強になりました!
Infinite ScrollでHeaderBidding導入を検討されている方の参考になれば幸いです。
それでは引き続き良いHeaderBiddingライフを!!!