広告配置仕様
Free プラン / 匿名ユーザーに対して Google AdSense 広告を表示する際の、対象ページ・スロット配置・広告形式・CLS 対策を定義する。
本仕様は ビジネスモデル・課金設計 の「ad-free を有料プランの付加価値にする」方針を実装に落とし込んだものであり、親 Issue #230 の実装着手前に配置設計を確定させる目的で作成する。
1. 概要
- 広告プロダクト: Google AdSense(手動配置 /
<ins class="adsbygoogle">) - Auto Ads は採用しない。理由は以下の 3 点:
- CLS 制御: 手動配置なら広告枠の
minHeightを固定できる - 配置品質: Auto Ads はコンテンツ構造を無視してオーバーレイを挿入するため UX を損なう
- ポリシー準拠: コンテンツ比率・誤クリック誘導を自前でコントロールする必要がある
- CLS 制御: 手動配置なら広告枠の
- Publisher ID:
ca-pub-2848807786857797(公開情報、apps/web/public/ads.txtに記載) - 収益化戦略上の位置付け: Free プランの持続可能性を広告収益で担保し、有料プラン(Plus / Pro)は「広告非表示」を付加価値として訴求する(
apps/web/src/views/home/ui/pricing-section/pricing-section.tsxのadFree行と整合)
2. 表示対象ユーザー
判定マトリクス
| ユーザー状態 | plan | 広告表示 |
|---|---|---|
| 匿名(未ログイン) | — | 表示 |
| ログイン済 Free | free | 表示 |
| ログイン済 Plus | plus | 非表示 |
| ログイン済 Pro | pro | 非表示 |
判定ロジック
- 新設する
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.tsのplanSchema = 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 の挿入条件を満たさない)場合でも違和感のないレイアウトを保つ- 各広告枠は
AdSlotのlayout="card"で描画し、VideoCard と同じ縦積み形状(aspect-video サムネ + メタ情報)にして周囲のグリッドと視覚的に調和させる - インフィード広告は検索結果カードと視覚的に近いスタイルにするが、「広告」ラベルを上部に必ず表示する(ポリシー準拠、ユーザー錯誤防止)
3.2 /videos/[videoId](動画詳細ページ)
- 主要導線:
/search→ 結果カードクリック →/videos/[videoId] - ページ特性: YouTube プレイヤー + 字幕ビューア + マッチハイライトで構成される縦長ページ
- 配置の狙い: プレイヤー視聴開始時と字幕読了後の 2 点で広告に触れさせる
スロット一覧
| ID | 位置 | 導線トリガー | 形式 | 最小高 |
|---|---|---|---|---|
video-detail-below-player | YouTube プレイヤーと字幕ビューアの間(列数分のカード型広告を横並び) | 再生開始時にスクロールなしで視界に入る | インフィード広告 | 300px |
video-detail-below-subtitle | 字幕ビューア末尾、関連動画セクション上(列数分のカード型広告を横並び) | 字幕読了後、次アクション前 | インフィード広告 | 300px |
注記
/searchの midroll / bottom と同じく、列数分(モバイル 2 / タブレット 3 / デスクトップ 4)のカード型広告を横並びで描画する。AdSlotのlayout="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-midroll | 300px |
search-result-bottom | 300px |
video-detail-below-player | 300px |
video-detail-below-subtitle | 300px |
- 広告が非配信(AdSense の在庫切れ、広告ブロッカー有効等)の場合でも、コンテナ要素は maintain する(空白のまま高さを保つ)
loading="lazy"はadsbygoogle.jsが自動付与するため、コンポーネント側での明示は不要- 「広告」ラベルは日本語で「広告」または英語で「Advertisement」を上部に表示する(ポリシー準拠)
5. 実装方針
実装詳細は #232 の PR で決めるが、本仕様で以下の原則を固定する:
- スクリプト読み込み:
adsbygoogle.jsはapps/web/src/app/[locale]/layout.tsxの<head>でnext/scriptのstrategy="afterInteractive"として 1 回だけ読み込む - 共通コンポーネント:
AdSlot(apps/web/src/shared/ui/ad-slot/)を新設。props でslotId/format/minHeightを受け取る - 非表示時の挙動:
useShouldShowAds()が false の場合、AdSlotは null を返す(空 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.tsx—adFree比較行apps/web/src/features/user-subscription/model/use-user-subscription/use-user-subscription.ts— プラン判定のベースpackages/shared/src/user/user-subscription/user-subscription.ts—planSchemaapps/web/messages/ja.json—comparison.rows.adFree