PWAで起動された時だけAdSense広告を出さない実装(matchMedia + navigator.standalone二重判定)

概要

今回はPWAスタンドアロンで起動された時だけ、AdSense広告を一切読み込まない実装を紹介していきます。

PWA化したアプリでホーム画面から起動すると、アドレスバーが消えてネイティブアプリのように動きますよね。
しかしそこに普通のWebサイト感覚でAdSenseが居座っていると、せっかくのアプリ感が一気に台無しになってしまいます。

そこで自分が運営しているcopype.shinpinoshi.comこぴぺったりでは、通常ブラウザ起動時は広告を出す/PWAスタンドアロン起動時は広告を出さないという出し分けをしています。

PWA対応したウェブアプリを作成している方の参考になればうれしいです!

それではやっていきましょう(^^!

目次

なぜPWAでは広告を出したくないのか

そもそもなぜわざわざ出し分けるのか、という背景から書いておきます。
PWAで広告非表示にしている理由は、大きく分けてアプリ感の維持vignette広告の×ボタンが押せなくなる問題の2つです。

アプリ感を壊したくない

PWAは「ホームに追加してアプリのように使う」のがウリの形態です。
ユーザーがホーム追加してまで使ってくれているということは、ほぼ確実にリピーター・ヘビーユーザー

  • リピーターは「いつものアプリ」として体験を期待している
  • そこに大きなバナー広告が居座ると、ネイティブアプリ風の体験が壊れる
  • スタンドアロン起動だと「戻る」「タブを閉じる」の逃げ道が限られていて、誤タップが離脱に直結する

特に最後の誤タップ問題は地味に効きます。
ブラウザならアドレスバーから戻れるけど、スタンドアロンPWAだとそうはいかないので、誤タップで広告ページに飛ばされた瞬間に「もういいや」となりがちです。

もしいい方法があれば導入するかもしれませんが、現状はこの体験劣化が大きすぎるのでオフにしています。

vignette広告の×ボタンが押せない

もう一つ実際にハマったのが、iOSのPWAスタンドアロン起動時、vignette広告(ページ遷移時の全画面広告)の×ボタンがsafe-area(ノッチやステータスバーの下)に潜り込んで押せなくなる挙動です。

  • 通常ブラウザだとアドレスバー分の余白があるので、×ボタンが安全領域より下に来て普通に押せる
  • PWA起動では画面いっぱいまでvignetteが伸びて、×がノッチやステータスバーの裏側に重なる
  • 結果として広告を閉じられず、ユーザーはアプリを一度落とすしか脱出手段がない

当初はscripts/build-static-pages.mjsadsenseSafeAreaPatchという、wrapper要素のtopをenv(safe-area-inset-top)で押し下げる対症療法を入れていました。
ただクロスオリジンiframe内の×ボタンは結局触りにくいうえ、PWAでは広告そのものを出さない方針に倒したので、今はそのパッチもフォールバックとして残してあるだけです。

なぜ出して隠すではなく、表示しない実装にしたのか?

display:noneすればいいじゃん」と思いますが、ここはAdSenseポリシー的にグレーです。

  • ・AdSenseは「広告コードの改変」「Invalid Trafficにつながる挙動」を禁止している
  • ・広告タグを読み込んでから隠すと、インプレッションは発生するが視認できない状態になりかねない
  • ・細かい線引きはケースバイケースだが、わざわざリスクを背負う必要はない

なので一番シンプルで安全なのは、そもそも広告スクリプトをロードしないこと。
こぴぺったりではこの方針で実装しています。

PWAをどう判定するか(matchMedia + navigator.standalone)

PWAスタンドアロンを判定する方法は、ブラウザによって2通りあります。

  • window.matchMedia('(display-mode: standalone)').matches
    • Chrome / Edge / Firefox / Android Chromeなど標準的なPWA対応ブラウザで使える
  • window.navigator.standalone === true
    • iOS Safariの独自フラグ。display-modeメディアクエリが効かない時代の名残

どちらか一方だけだと、iOS or Androidのどちらかで取り逃します。
なので両方をORで組み合わせる「二重判定」としています。

PWA判定の最小コード
1
2
3
const isPwa =
(window.matchMedia && matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;

window.matchMediaの存在チェックも入れておくと、古い環境でも例外を出さずにすみます!

実装①: AdSenseスクリプトをそもそも読み込まない

こぴぺったりは静的ページ(/about, /features, /guide など)をビルド時に生成しているので、scripts/build-static-pages.mjsの中でPWA時はscriptタグを追加せずreturnする処理をインラインスクリプトとして埋め込んでいます。

scripts/build-static-pages.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const adsenseScript = adsenseEnabled
? `<script>
(function () {
var isPwa = (window.matchMedia && matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;

function removeAdSlots() {
var wrappers = document.querySelectorAll('.ad-bottom');
for (var i = 0; i < wrappers.length; i++) wrappers[i].remove();
}

if (isPwa) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', removeAdSlots);
} else {
removeAdSlots();
}
return; // ← ここで広告ロードを完全スキップ
}

var s = document.createElement('script');
s.async = true;
s.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=' + ADSENSE_PUB_ID;
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
})();
</script>`
: '';

ポイントを整理するとこんな感じ。

  • IIFE(即時実行関数)でローカルスコープに閉じ込める
  • 先頭でPWA判定 → trueなら広告スロットDOMを削除してreturn
  • document.readyStateを見て、まだHTML解析中ならDOMContentLoadedを待ってから削除
  • 通常ブラウザのときだけadsbygoogle.js<script>タグで動的に追加

removeAdSlotsは静的HTMLにあらかじめ書かれている.ad-bottomの枠を取り除く処理です。
広告スクリプトを読まないだけでなく、広告枠の余白自体も消えるので、PWA起動時のレイアウトが綺麗にまとまります。

実装②: 広告ユニット側でも push を発火させない

スクリプトロード側で対策していても、広告ユニット側のadsbygoogle.push({})が走ると意図しない挙動を招くので、ユニット側にも保険でPWA判定を入れています。

広告ユニット部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const adsenseUnit = adsenseEnabled && SLOT_ID
? `<div class="ad-bottom">
<ins class="adsbygoogle"
style="display:block"
data-ad-client="${PUB_ID}"
data-ad-slot="${SLOT_ID}"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>
(function () {
var isPwa = (window.matchMedia && matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;
if (isPwa) return;
(window.adsbygoogle = window.adsbygoogle || []).push({});
})();
</script>
</div>`
: '';

ロジックは先ほどとほぼ同じですが、こちらはPWA時にpush({})を呼ばないだけです。

  • 実装①でDOMごと消しているので、本来ここまで来ない
  • ただし「①の削除より先にこのスクリプトが走る」可能性も0ではない
  • 念のため二重で守っておくと安心

かなり神経質になように見えますが、AdSense関連は事故ったときのリカバリが面倒で最悪不可能になる領域なので、コストの低い保険は入れる派です。

動作確認

実装が終わったら、本番ビルドして実機で確認します。
PWA挙動はnpm run devでは出ないので、必ずビルドしてからチェックします。

  • 通常ブラウザ
    • 該当ページを開いて、広告が表示される&Networkタブにadsbygoogle.jsへのリクエストがあること
  • PWA起動(Android Chrome)
    • ホーム画面に追加 → アイコンから起動 → 広告枠が消えていて、adsbygoogle.jsのリクエストが飛んでいないこと
  • PWA起動(iOS Safari)
    • 共有メニュー → 「ホーム画面に追加」 → 起動して同じく広告がないこと
  • DevToolsで擬似PWA確認
    • Chrome DevTools → 「Application」 → 「Manifest」 → 「Open in window」で、スタンドアロン状態を再現できる

iOSとAndroidは判定APIが違うので、両方のOSでホーム追加して起動するまで確認すると安心です。

ハマったポイント

npm run devではdisplay-mode: standaloneが効かない

vite-plugin-pwaのdevOptionsを切っているとService Workerが動かないので、ホーム追加もできず、PWAスタンドアロン判定もtrueになりません。

確認は必ずnpm run build && npm run previewまたは本番ドメインで
私はこれで30分くらい「あれ、判定が動かない…」と悩みました(- -

インラインスクリプトをテンプレートリテラルで書くときのエスケープ

build-static-pages.mjsの中で、AdSenseの<script>...</script>をJSのテンプレートリテラルで書いているので、

  • 内側のJSで${...}を使うとテンプレ側のリテラル展開と衝突する
  • バッククォートをそのまま使うと閉じてしまう

このあたりは普通の文字列リテラルやString.raw、もしくは外部ファイルから読み込む方式に切り替えるとハマりにくくなります。
こぴぺったりではテンプレートリテラルのまま書いていますが、外側で${...}を使うのは設定値(PUB_ID, SLOT_ID)だけにとどめています。

SPA本体には元々AdSenseを入れていない

これは設計判断ですが、SPA本体(Reactアプリ部分)にはAdSenseをそもそも入れていません

  • アプリ操作画面に広告は出さない(UX優先)
  • 広告は静的ページ(/about, /features, /guide, /privacy など)にだけ載せる
  • なのでこの記事のPWA出し分けロジックは「静的ページ側だけ」で完結する

まとめ

いかがでしたか?
PWA対応は通常webアプリとは勝手が違う面が多いので、広告もその違いにの一つですが、
やらないことも一つの判断だと思ってます!

ここで紹介したことをサクッとまとめると

  • ・PWAでは「アプリ感を壊さない」「誤タップ離脱を避ける」観点で広告を出さないのが妥当
  • ・AdSenseポリシー的にも「出して隠す」より「そもそも出さない」が安全
  • ・PWA判定はmatchMedia('(display-mode: standalone)') + navigator.standaloneの二重判定
  • ・スクリプトロード時にreturnで広告JS自体を読み込まない(実装①)
  • ・念のため広告ユニット側でもadsbygoogle.pushを発火させない(実装②)
  • ・確認はbuild && previewもしくは実機ホーム追加で

といった感じです。

実物はcopype.shinpinoshi.comこぴぺったりで動いているので、PCブラウザで/aboutを開いた時とスマホでホーム追加して起動した時を見比べると分かりやすいです。

PWA化したけど広告どうしよう、と悩んでいる人の参考になれば嬉しいです。

以上となります。
広告収入より使ってもらいたい欲が大きい今日この頃
それではお疲れさまでした!