メインコンテンツまでスキップ

広告配置仕様

Free プラン / 匿名ユーザーに対して Google AdSense 広告を表示する際の、対象ページ・スロット配置・広告形式・CLS 対策を定義する。

本仕様は ビジネスモデル・課金設計 の「ad-free を有料プランの付加価値にする」方針を実装に落とし込んだものであり、親 Issue #230 の実装着手前に配置設計を確定させる目的で作成する。

1. 概要

  • 広告プロダクト: Google AdSense(手動配置 / <ins class="adsbygoogle">
  • Auto Ads は採用しない。理由は以下の 3 点:
    1. CLS 制御: 手動配置なら広告枠の minHeight を固定できる
    2. 配置品質: Auto Ads はコンテンツ構造を無視してオーバーレイを挿入するため UX を損なう
    3. ポリシー準拠: コンテンツ比率・誤クリック誘導を自前でコントロールする必要がある
  • Publisher ID: ca-pub-2848807786857797(公開情報、apps/web/public/ads.txt に記載)
  • 収益化戦略上の位置付け: Free プランの持続可能性を広告収益で担保し、有料プラン(Plus / Pro)は「広告非表示」を付加価値として訴求する(apps/web/src/views/home/ui/pricing-section/pricing-section.tsxadFree 行と整合)

2. 表示対象ユーザー

判定マトリクス

ユーザー状態plan広告表示
匿名(未ログイン)表示
ログイン済 Freefree表示
ログイン済 Plusplus非表示
ログイン済 Propro非表示

判定ロジック

  • 新設する useShouldShowAds() フックで判定する(実装は #232
  • 内部で既存の useUserSubscription()apps/web/src/features/user-subscription/model/use-user-subscription/use-user-subscription.ts)を利用する
  • 匿名ユーザー時は useUserSubscription() の query が無効(enabled=false)になるため、フォールバックとして Free 相当で扱う
  • プランの定義は packages/shared/src/user/user-subscription/user-subscription.tsplanSchema = z.enum(["free", "plus", "pro"]) に準拠

3. 対象ページとスロット配置

広告を表示するのは /search/videos/[videoId] の 2 ページのみ。

3.1 /search(検索結果ページ)

  • 主要導線: /(ヘッダー検索 or CTA)→ /search?q=...、または検索履歴・直接リンク
  • ページ特性: サービス内で最長のスクロールを生むページ。検索結果を Grid 形式で無限スクロール風に表示するため、ユーザー滞在時間が長い
  • 配置の狙い: スクロール深度が深いユーザーを自然な位置で捕捉する

スロット一覧

ID位置導線トリガー形式最小高
search-result-midroll結果グリッドの 3 行ごと に 1 行(列数分=2/3/4 枠のカード型広告を横並び)検索実行 → 結果をスクロール中に自然視界に入るインフィード広告300px
search-result-bottom結果リスト末尾、ページネーション直前(列数分=2/3/4 枠のカード型広告を横並び)スクロール終端インフィード広告300px

注記

  • 空状態(0 件ヒット、未検索)時は no-results カードの直下に 1 行分(列数分のカード型 midroll 広告) のみ表示する。search-result-bottom は表示しない(結果リストが無いため末尾広告は省略)
  • search-result-midroll の挿入頻度は 3 行ごとに 1 行 で統一する。1 行あたり 列数分のカード型広告を並べる(モバイル 2col = 2 枠、タブレット 3col = 3 枠、デスクトップ 4col = 4 枠)
    • 実装は useGridColumns() で現在の列数を検出し、shouldInsertInFeedAd(index, cols, totalHits) で行境界を判定する
    • 最終行の直後には挿入しない(広告の後にコンテンツがないため)
    • SSR / 初回マウント前は null を返し広告を描画しないことで hydration 差分を防ぐ
  • search-result-bottom も midroll と同じく 列数分のカード型広告を 1 行横並びで描画する。周囲の VideoCard グリッドと揃うことで、ヒット件数が少ない(midroll の挿入条件を満たさない)場合でも違和感のないレイアウトを保つ
  • 各広告枠は AdSlotlayout="card" で描画し、VideoCard と同じ縦積み形状(aspect-video サムネ + メタ情報)にして周囲のグリッドと視覚的に調和させる
  • インフィード広告は検索結果カードと視覚的に近いスタイルにするが、「広告」ラベルを上部に必ず表示する(ポリシー準拠、ユーザー錯誤防止)

3.2 /videos/[videoId](動画詳細ページ)

  • 主要導線: /search → 結果カードクリック → /videos/[videoId]
  • ページ特性: YouTube プレイヤー + 字幕ビューア + マッチハイライトで構成される縦長ページ
  • 配置の狙い: プレイヤー視聴開始時と字幕読了後の 2 点で広告に触れさせる

スロット一覧

ID位置導線トリガー形式最小高
video-detail-below-playerYouTube プレイヤーと字幕ビューアの間(列数分のカード型広告を横並び)再生開始時にスクロールなしで視界に入るインフィード広告300px
video-detail-below-subtitle字幕ビューア末尾、関連動画セクション上(列数分のカード型広告を横並び)字幕読了後、次アクション前インフィード広告300px

注記

  • /search の midroll / bottom と同じく、列数分(モバイル 2 / タブレット 3 / デスクトップ 4)のカード型広告を横並びで描画する。AdSlotlayout="card" を使用し、grid-cols-2 sm:grid-cols-3 md:grid-cols-4 でレスポンシブに折り返す
  • ダイレクト広告仕様と整合させるため、固定 4 position でレンダーする(デスクトップは 1 行 × 4 枠、モバイルは 2 行 × 2 枠に折り返し)
  • video-detail-below-playerプレイヤー直下ゆえに誤クリックのリスクが高いため、上下に margin: 16px 以上の余白を設ける
  • 字幕ビューア内部にミドルイン広告を挿入する案は、字幕読書体験を分断するため 初回実装では見送る
  • 右サイドバーが存在しても、初回実装では広告枠として利用しない(レイアウトの視認性を優先)

3.3 対象外ページとその理由

ページ除外理由
/(ホーム)CTA / Pricing への導線が密集しており、広告で離脱率を増やしたくない
/account/*集中度の高い短尺ページ、ユーザーの作業体験を妨げたくない
/login認証ページは摩擦を最小化する
/admin/*運用管理用、対象外
/privacy, /terms, /contact規約系・低流量、表示メリットが小さい
/changelog低流量、運用コストに見合わない

4. 広告形式と CLS 対策

4.1 使用する広告形式

  • レスポンシブディスプレイ広告: data-ad-format="auto" + data-full-width-responsive="true"。画面幅に応じて 320x50〜970x250 の範囲で自動選択
  • インフィード広告: 検索結果グリッドに自然に溶け込むカード風スタイル。data-ad-format="fluid" + data-ad-layout-key を AdSense 管理画面で発行
  • マルチプレックス広告: data-ad-format="autorelaxed"。関連コンテンツ風に複数広告をグリッド表示

4.2 CLS 対策

スロットごとに minHeight を固定し、広告ロード前後でレイアウトシフトが発生しないようにする。リポジトリの .claude/rules/web-performance.md で定めた「動的コンテンツの高さ確保」方針に準拠。

スロットminHeight
search-result-midroll300px
search-result-bottom300px
video-detail-below-player300px
video-detail-below-subtitle300px
  • 広告が非配信(AdSense の在庫切れ、広告ブロッカー有効等)の場合でも、コンテナ要素は maintain する(空白のまま高さを保つ)
  • loading="lazy"adsbygoogle.js が自動付与するため、コンポーネント側での明示は不要
  • 「広告」ラベルは日本語で「広告」または英語で「Advertisement」を上部に表示する(ポリシー準拠)

5. 実装方針

実装詳細は #232 の PR で決めるが、本仕様で以下の原則を固定する:

  • スクリプト読み込み: adsbygoogle.jsapps/web/src/app/[locale]/layout.tsx<head>next/scriptstrategy="afterInteractive" として 1 回だけ読み込む
  • 共通コンポーネント: AdSlotapps/web/src/shared/ui/ad-slot/)を新設。props で slotId / format / minHeight を受け取る
  • 非表示時の挙動: useShouldShowAds() が false の場合、AdSlotnull を返す(空 div も出さない)
  • 環境変数: NEXT_PUBLIC_ADSENSE_CLIENT_ID が未設定の場合は本番ビルドでも広告をレンダーしない。#231 の審査完了前でもローカル開発・Preview デプロイを阻害しないため

6. プライバシー・コンプライアンス

AdSense CMP(同意管理プラットフォーム)

  • 選択: Google CMP、3 択(同意する / 同意しない / オプションを管理する)
  • 適用範囲: EEA(欧州経済領域) / 英国 / スイスのユーザーのみに表示される。日本・米国等のユーザーには CMP は表示されない
  • 拒否時の挙動: 非パーソナライズ広告(NPA / Non-Personalized Ads)にフォールバックし、広告配信は継続される。収益は約 30〜70% 低下するが、ゼロにはならない

プライバシーポリシー

  • apps/web/src/app/[locale]/privacy/page.tsx に AdSense 利用の記載を追加する必要がある(実装 PR である #233 / #234 着手時に対応)
  • 記載内容: 配信事業者、Cookie 利用目的、オプトアウト方法

7. 検証観点

計測と閾値検証は #235 で実施する。

観点閾値 / 方法
CLS< 0.1(Lighthouse / Chrome DevTools Performance)
LCP広告追加前後で +200ms 以内(SpeedInsights の 75 パーセンタイル)
プラン別表示匿名 / Free / Plus / Pro の 4 状態で 2 ページを巡回、表示・非表示が仕様通りか目視確認
ポリシー「広告」ラベル、誤クリック誘導なし、コンテンツ比の妥当性
広告ブロッカーuBlock Origin 有効時にレイアウト崩れが発生しないこと

8. 関連 Issue / 参考資料

Issue

既存ファイル

  • apps/web/src/views/home/ui/pricing-section/pricing-section.tsxadFree 比較行
  • apps/web/src/features/user-subscription/model/use-user-subscription/use-user-subscription.ts — プラン判定のベース
  • packages/shared/src/user/user-subscription/user-subscription.tsplanSchema
  • apps/web/messages/ja.jsoncomparison.rows.adFree

外部ドキュメント