📊

Google AdManager(GAM)を使ってNext.jsサイトに広告を配信する

2022/09/19に公開

先日、私が運営しているNext.js製の趣味ブログがAdSenseの審査に合格したため、広告が配信できるようになりました。
https://ebifran-roadbike-blog.com/

AdSenseのタグを直接貼り付けることで広告を配信してもよかったのですが、せっかくなのでGoogle AdManager(以下、GAM)を使ってAdSense広告を配信するようにしてみました。
ここではNext.jsサイトにGAMを導入するにあたって得た知見を残します。

またあらかじめ書いておくと、自分のような規模の小さい趣味ブログ、かつAdSense広告しか配信しない場合においてはGAMを導入するメリットはほぼ無いと思います。興味本位でやってみたという感じなので参考程度にお願いします。

(2022/09/23 コード修正)
記事作成時点からコードを一部リファクタしたので、記事内の一部のコードと本文を修正しました。

Google AdManager(GAM)とは

GAMとは、Googleが提供しているアドサーバーのことです。
基本的にはPV数が多く膨大な広告を扱う媒体サイトで広告を管理するために使われるものです。

以下のような使い方が基本だと思います。

  • 配信日時をあらかじめ設定しておき、決まった時間に広告配信するようにする
  • 広告枠の在庫がある場合にネットワーク広告を配信する

また複数の広告事業者と入札単価を競わせることによって収益を向上させることもできるようですが、そのあたりはまだ調べ切れておらずそこまでの設定はしていません。(広告事業者と契約する必要があると思うので個人では難しいような気がする)

GAMはインプレッション数(広告が表示された回数)が既定の回数以下であれば無料で利用可能です。無料インプレッション数の上限については以下に記載があります。
https://support.google.com/admanager/answer/6214526?hl=ja

地域によって異なるようですが、日本の場合は月間で1億5000万回のインプレッションまで無料で使えるようです。大体の個人が無料枠内で収まるでしょう。

Google Publisher Tag(GPT)について

GAMで広告を配信するためには、Google Publisher Tag(以下、GPT)と呼ばれるjsコードを広告配信したいサイト上に実装していく必要があります。
リファレンスは英語のみですが、分かりやすいサンプルとデモページが用意されているので理解しやすいです。

通常のWEBサイトであればサンプルに記載の通りのコードをhtml上に設定するだけでほぼ問題なく配信できるようになりますが、Next.js(というよりSPAサイト全般)においては注意が必要な点がいくつかあります。詳細は後述します。

実装手順

ここからはGAM上の設定と実装するGPTタグについての詳細を記載してきます。

型定義ファイルをインストール

typescriptで実装を楽に進めていくために、まずはGPTの型定義ファイルをインストールします。
型定義ファイルはgoogleが公式に提供しており、以下のコマンドを叩けばインストール可能です。

npm install --save-dev @types/google-publisher-tag

これでGPTの入力補完が効くようになります。

GAM上で広告ユニットを作成する

まずはサイト上に設置する広告枠に関する情報を「広告ユニット」という形でGAMに登録していきます。

私のブログの場合、以下の通りページ上に最大4つの広告枠が存在します(青枠で囲っている部分)。これに相当する広告ユニットを作成していきます。

  • ページ上部

  • ページ下部

今回の手順ではGPTの実装をメインで扱うため、GAM上の設定手順については他のサイトに任せることとして省略します。すべての広告ユニットを作成した状態が以下です。

1つの広告ユニットにサイズを複数設定しているのは、サイトを閲覧している端末のviewport幅によって配信する広告のサイズを切り替えたいためです。
ただしこれは枠ごとに配信を許可するサイズを設定しているだけで、端末のviewport幅に応じて適切なサイズの広告を配信する設定ではありません。レスポンシブに広告を配信したい場合はGPTで実装を行う必要があります(後述します)。

通常であればオーダーや広告申込情報といった情報をGAMに追加で登録する必要がありますが、AdSense広告のみを配信するのであれば上記の広告ユニットの登録だけ済ませればGAM上の設定は完了です。

gpt.jsを読み込ませる

ページ上にGPTを実装するにあたり、googleが提供しているgpt.jsというjsファイルを読み込ませる必要があります。

今回は以下のように/components/gpt/index.tsxGptHeadというReactコンポーネントを作りました。

/components/gpt/index.tsx
import Script from "next/script";

export const GptHead: React.FC = () => {
  return (
    <>
      <Script
        async
        src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"
        strategy="afterInteractive"
      />
      <Script
        id="gpt-head"
        dangerouslySetInnerHTML={{
          __html: `window.googletag = window.googletag || { cmd: [] };`,
        }}
      />
    </>
  );
};

作成したGptHeadをNext.jsの_app.tsx内で参照することでjsの読み込みが完了です。

_app.tsx
return (
    <Container>
        <GptHead />
        <Component {...pageProps} />
    </Container>
)

広告ユニットを表すオブジェクトの作成

GAM上に設定した各広告ユニットに対応するオブジェクトを作成します。今回は以下のように/components/gpt/adunit.tsxを作成しました。長くなりますがコード全体を記載します。

/components/gpt/adunit.tsx
export type SingleSizeArray = [number, number];
export type NamedSize = "fluid" | ["fluid"];

export type SingleSize = SingleSizeArray | NamedSize;
export type MultiSize = SingleSize[];
export type GeneralSize = SingleSize | MultiSize;

export class AdUnit {
  constructor(
    public unitName: UnitName,
    public sizes: GeneralSize,
    public sizeMappings: SizeMapping[],
    public minHeight: Number
  ) {}

  get divName() {
    return `div-gpt-ad-${this.unitName.replace("/", "-")}`;
  }
}

class SizeMapping {
  constructor(
    public viewportSize: SingleSizeArray,
    public slotSize: GeneralSize
  ) {}
}

export type UnitName = "top" | "side" | "bottom" | "contents";

/**
 * 上部バナー
 */
export const TopUnit = new AdUnit(
  "top",
  [
    [970, 90],
    [728, 90],
    [468, 60],
    [320, 50],
  ],
  [
    {
      viewportSize: [992, 744],
      slotSize: [970, 90],
    },
    {
      viewportSize: [768, 576],
      slotSize: [728, 90],
    },
    {
      viewportSize: [517, 387],
      slotSize: [468, 60],
    },
    {
      viewportSize: [0, 0],
      slotSize: [320, 50],
    },
  ],
  50
);

/**
 * サイドバナー
 */
export const SideUnit = new AdUnit(
  "side",
  [
    [300, 600],
    [160, 600],
    [336, 280],
    [300, 250],
  ],
  [
    {
      viewportSize: [1280, 960],
      slotSize: [300, 600],
    },
    {
      viewportSize: [992, 744],
      slotSize: [160, 600],
    },
    {
      viewportSize: [0, 0],
      slotSize: [
        [336, 280],
        [300, 250],
      ],
    },
  ],
  250
);

/**
 * 下部バナー
 */
export const BottomUnit = new AdUnit(
  "bottom",
  [
    [750, 300],
    [750, 200],
    [336, 280],
    [300, 250],
  ],
  [
    {
      viewportSize: [1280, 960],
      slotSize: [
        [750, 300],
        [750, 200],
      ],
    },
    {
      viewportSize: [0, 0],
      slotSize: [
        [336, 280],
        [300, 250],
      ],
    },
  ],
  200
);

/**
 * 記事ページ目次下バナー
 */
export const ContentsUnit = new AdUnit(
  "contents",
  [
    [750, 300],
    [750, 200],
    [336, 280],
    [300, 250],
  ],
  [
    {
      viewportSize: [1280, 960],
      slotSize: [
        [750, 300],
        [750, 200],
      ],
    },
    {
      viewportSize: [0, 0],
      slotSize: [
        [336, 280],
        [300, 250],
      ],
    },
  ],
  200
);

広告ユニットを表すAdUnitクラスと、GAM上に設定した各広告ユニットに対応するオブジェクトを定義しています。AdUnitに設定したプロパティの説明は以下です。

  • unitName: GAMの広告ユニットに設定した「コード」と一致
  • sizes: GAMの広告ユニットに設定した「サイズ」と一致
  • sizeMappings: レスポンシブに広告配信するために必要な設定。設定内容はサイトのデザイン(レスポンシブデザインのブレークポイントの設定など)によって異なる
  • minHeight: Cumulative Layout Shift(CLS)を最小限にするため、サイト上の広告枠に最低限確保させる高さ

sizeMappingsに定義したviewportの設定については以下に詳しい説明があります。
https://developers.google.com/publisher-tag/guides/ad-sizes

ページ内に広告ユニットを設置

上で定義した広告ユニットを各ページ内に設置していきます。

GAM上の広告ユニットを選択してタグを生成するとわかりますが、以下のようなdivタグを広告枠を表示したい箇所に設置することになります。
GPTのgoogletag.displayをdivのidを指定して呼び出すことで、対象のdivタグに広告を配信するという内容です。

ドキュメントの本文
<div id='div-gpt-ad-【固有のID】-0' style='min-width: 160px; min-height: 250px;'>
  <script>
    googletag.cmd.push(function() { googletag.display('div-gpt-ad-【固有のID】-0'); });
  </script>
</div>

設置するにあたり、作成済みの/components/gpt/index.tsxに以下の定義を追加します。
なおこの例ではChakra UIのBoxを使った実装になっていますが、適宜お使いのUIライブラリに応じて変更してください。

/components/gpt/index.tsx
import { AdUnit } from "./adunit";
import { Box, BoxProps } from "@chakra-ui/react";

export type GptBodyProp = {
  adUnit: AdUnit;
} & BoxProps;
export const GptBody: React.VFC<GptBodyProp> = ({ adUnit, ...props }) => {
  return (
    <Box
      id={adUnit.divName}
      textAlign="center"
      maxW="100%"
      minH={`${adUnit.minHeight}px`}
      {...props}
    />
  );
};

上で作ったGptBodyを広告ユニットを設置したい箇所で以下のように呼び出すことで、広告ユニットに応じたdivタグを設置できます。

import { TopUnit } from "components/gpt/adunit";
import { GptBody } from "components/gpt";

<GptBody adUnit={TopUnit} />

これで広告を配信するのに必要なdivタグの設置が完了しました。
ただし広告配信に必要なGPTの実装がまだ完了していないので、この段階では広告は表示されません。

ページ上に設置する広告情報を定義

私のブログは、以下の通りページによって広告枠のパターンが2パターンに分かれます。

  • 一覧ページ: 広告枠3つ
  • 記事詳細ページ: 広告枠4つ(一覧ページの3枠に加えて、目次の下に1枠)

この違いは後述のGPTの実装に影響してくるので、以下のように/components/gpt/adslot.tsxに実装しました。

/components/gpt/adslot.tsx
import { BottomUnit, ContentsUnit, SideUnit, TopUnit } from "./adunit";

export enum SlotType {
  List = 1,
  ArticleDetail = 2,
}

class AdSlots {
  constructor(private adUnits: AdUnit[]) {}
  
  // todo
}

const AdSlotsByType: Record<SlotType, AdSlots> = {
  // 一覧ページのslot
  [SlotType.List]: new AdSlots([TopUnit, SideUnit, BottomUnit]),
  
  // 記事詳細ページslot
  [SlotType.ArticleDetail]: new AdSlots([
    TopUnit,
    ContentsUnit,
    SideUnit,
    BottomUnit,
  ]),
};

広告配信する各ページでAdSlotsByTypeをenum指定で参照することで、配信する広告情報を表すAdSlotsのオブジェクトが取得できるようになっています。このオブジェクトを使ってGPTの呼び出しを行うため、AdSlotsクラスに実装を追加していきます。

残りのGPTタグの実装

GAM上の広告ユニットを選択して広告タグを生成すると、「ドキュメントのヘッダー」の部分に以下のようなタグが生成されます。
これらのタグをページ内に実装することになります。

ドキュメントのヘッダー
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
  window.googletag = window.googletag || {cmd: []};
  googletag.cmd.push(function() {
    googletag.defineSlot('/【固有のID】/side', [[160, 600], [336, 280], [300, 250], [300, 600]], 'div-gpt-ad-1663560505693-0').addService(googletag.pubads());
    googletag.pubads().enableSingleRequest();
    googletag.enableServices();
  });
</script>

この中のgoogletag.defineSlotは、ページ内にどんな広告ユニットが存在するのかを定義するための関数です。googletag.defineSlotの内容と、body内に設置したdivタグの内容が一致していないと正常に広告配信されません。

また広告ユニットを選択して生成したタグではgoogletag.defineSlotの呼び出しが1回のみになっていますが、ページ内に設置する広告ユニットの数だけ呼び出しを行う必要があります。その際、以下のような感じでgoogletag.defineSlotの呼び出しは一気に行う必要があるので注意が必要です。
(enableSingleRequestよりも前にすべてのunit分実行しておく必要がある)

googletag.cmd.push(function() {
    googletag.defineSlot(【上部バナーの情報】).addService(googletag.pubads());
    googletag.defineSlot(【サイドバナーの情報】).addService(googletag.pubads());
    googletag.defineSlot(【下部バナーの情報】).addService(googletag.pubads());
    googletag.pubads().enableSingleRequest();
    googletag.enableServices();
  });

この実装を行うために、作成済みの/components/gpt/adslot.tsxに対して以下の実装を行いました。

/components/gpt/adslot.tsx
export const useHeadScript = (slotType?: SlotType) => {
  const router = useRouter();

  // ページ遷移時に現在のslot情報を破棄
  useEffect(() => {
    router.events.on("routeChangeStart", removeSlots);
    return () => {
      router.events.off("routeChangeStart", removeSlots);
    };
  }, []);

  // ページに合わせたdefineSlot呼び出し
  useEffect(() => {
    if (slotType) {
      const adSlots = AdSlotsByType[slotType];
      adSlots.callHeadScript();
    }
  }, [router.asPath]);
};

// destroySlotsの呼び出し
const removeSlots = function () {
  googletag.cmd.push(function () {
    googletag.destroySlots();
  });
};

class AdSlots {
  constructor(private adUnits: AdUnit[]) {}

  // GPTの呼び出し
  callHeadScript() {
    const _adUnits = this.adUnits;
    googletag.cmd.push(function () {
      // defineSlot
      for (const unit of _adUnits) {
        const slot = googletag.defineSlot(
          `【固有のID】/${unit.unitName}`,
          unit.sizes,
          unit.divName
        );

        if (slot) {
          const builder = googletag.sizeMapping();
          for (const sizeMapping of unit.sizeMappings) {
            builder.addSize(sizeMapping.viewportSize, sizeMapping.slotSize);
          }

          const mapping = builder.build();
          if (mapping) {
            slot.defineSizeMapping(mapping);
          }

          slot.addService(googletag.pubads());
        }
      }

      googletag.pubads().enableSingleRequest();
      googletag.enableServices();

      googletag.display(_adUnits[0].divName);
    });
  }
}

googletag.sizeMappingというGPT関数を追加で呼び出しています。これが端末のviewport幅に応じて配信する広告サイズを切り替えるための関数です。

googletag.displayについてもこのタイミングで呼び出しています。広告ユニット1回分しか呼び出していないですが、enableSingleRequestしている場合はこれでページ内の全広告ユニットに配信が行われるようです。

重要なのが、ページ遷移のタイミングでgoogletag.destroySlotsを呼び出すようにすることです。
通常のWEBサイトであればページ遷移時にdomが完全に初期化されるため考慮しないくて良いですが、SPAの場合はページ遷移後も遷移前のスロット情報が残り続けてしまい、広告がリフレッシュされなくなってしまうので対応が必要です。

またそれ以外のGPTについては、useEffectの依存配列にrouter.asPathを渡すことで、URLが変わるごとに呼び出されるようにしています。

上で実装したuseHeadScriptを、各ページのReactコンポーネントで呼び出せば実装が完了です。実際にはLayoutページなどで呼び出すことになるでしょう。

import { SlotType, useHeadScript } from "components/gpt/adslot";

const Page: NextPage = () => {
  // 一覧ページの場合
  useHeadScript(SlotType.List);
  
  return (
    ...
  );
};

動作確認

ここまでの実装が正常に完了していれば、広告配信に必要な実装はすべて完了しているはずです。
必要な設定が正しくできているかの確認は、以下をブックマークレットに登録して実行することで行います。

javascript:googletag.openConsole()

このブックマークレットを実行すると、以下のようにブラウザ内に広告確認用のconsoleが立ち上がります。
ローカルデバッグだとAdSense広告は表示されていないと思いますが、「ページリクエスト」のタブ内に「ページのタグ設定に問題はありません。」と表示されていればOKです。

また、「広告スロット」タブにはページに表示中の広告ユニットの一覧が表示されます。

まとめ

GAMを使ってNext.jsサイトに広告配信をするための手順について説明しました。
自分が調べた限りではGAMとNext.jsのつなぎこみに関する日本語のページは無かったので、いろいろ試行錯誤しながらの実装になりました。
このページが何かの参考になればうれしいです。

Discussion