CreaTools LogoCreaTools
Tips

Next.js(App Router)でAdSenseが動かない原因と対策

2026-01-05

この記事が解決する状況

以下のどれかに当てはまるなら、この記事を読む価値がある。

状況読むべきセクション
AdSenseコードを貼ったのに広告が出ないなぜ難しいか → 実装
adsbygoogle.push() error が出る設計方針 → 広告コンポーネント
ページ遷移すると広告が消えるページ遷移対応
審査に落ちた、または審査が不安審査に落ちるパターン
Vercelにデプロイしているが動かないVercel特有の注意点

すでにHTMLサイトでAdSenseを動かした経験があり、Next.jsで同じように実装しようとして詰まっている人向け。


なぜNext.jsでAdSenseが難しいか

「AdSenseのコードをそのまま貼ったら動かない」

HTMLサイトなら動くコードが、Next.jsでは動かない。理由はSPAの仕組みにある。

問題原因
広告が出ないスクリプトの読み込みタイミング
二重pushエラーStrict Modeで2回レンダリング
ページ遷移で消えるソフトナビゲーションでDOMが残る
ハイドレーションエラーSSRとCSRの不一致

これらを理解せずに実装すると、動いたり動かなかったりする不安定な状態になる。


審査に落ちるパターン

AdSense審査で落ちる原因の多くは、Next.js特有ではなく「サイト構成」の問題。ただし、SPAゆえに見落としやすい点がある。

パターン1: コンテンツ不足と判定される

症状: 「有用性の低いコンテンツ」で却下
原因: Googleクローラーがページを正しくレンダリングできていない

SPAは初回HTMLが空に近い。クローラーがJSを実行しないと、コンテンツがないように見える。

対策:

  • generateStaticParams でSSG/ISRを活用し、HTMLにコンテンツを含める
  • robots.txt でクローラーのアクセスを確認
  • Google Search Console の「URL検査」でレンダリング結果を確認

パターン2: ads.txtが認識されない

症状: 審査は通るが広告が出ない、または審査で「ads.txt問題」
原因: Vercelのルーティングでads.txtが正しく配信されていない

対策: 後述の「Vercel特有の注意点」を参照。

パターン3: ポリシー違反に気づかない構成

症状: 「ポリシー違反」で却下
原因: 動的ルーティングで同一コンテンツが複数URLに存在

Next.jsの [slug] 等で、同じ内容が異なるURLでアクセス可能になっていないか確認。canonical設定を忘れずに。


設計方針を先に決める

スクリプト読み込み

方法判断
<script>直書き✗ Next.jsでは非推奨
next/script + afterInteractive✓ 採用

afterInteractive = ハイドレーション後に読み込む。ページ表示を妨げない。

広告コンポーネント

方法判断
useEffectで直接push✗ 二重実行のリスク
useRefで実行済みフラグ✓ 採用

Strict ModeではuseEffectが2回実行される。フラグなしだとpush()が2回呼ばれてエラー。

ページ遷移対応

方法判断
対策なし✗ 遷移後に広告が出ない
keyにpathnameを渡して再マウント✓ 採用

実装

1. スクリプトを layout.tsx に配置

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        {children}
        
        <Script
          async
          src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"
          crossOrigin="anonymous"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

全ページで1回だけ読み込まれる。

2. 型定義を追加

// types/adsense.d.ts
declare global {
  interface Window {
    adsbygoogle: { push: (params?: Record<string, unknown>) => void }[];
  }
}

export {};

@ts-expect-errorを排除。tsconfig.jsonincludeにこのファイルが含まれていることを確認。

3. 広告コンポーネント

// components/AdUnit.tsx
"use client";

import { useEffect, useRef, useState } from "react";

type Props = {
  slot: string;
  format?: "auto" | "rectangle" | "horizontal" | "vertical";
};

export default function AdUnit({ slot, format = "auto" }: Props) {
  const [mounted, setMounted] = useState(false);
  const adPushed = useRef(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  useEffect(() => {
    if (!mounted) return;
    if (adPushed.current) return;
    adPushed.current = true;

    const timer = setTimeout(() => {
      try {
        window.adsbygoogle = window.adsbygoogle || [];
        window.adsbygoogle.push({});
      } catch (e) {
        if (process.env.NODE_ENV === "development") {
          console.error("AdSense error:", e);
        }
      }
    }, 100);

    return () => clearTimeout(timer);
  }, [mounted]);

  if (!mounted) return null;

  return (
    <ins
      className="adsbygoogle"
      style={{ display: "block", width: "100%", minHeight: 250 }}
      data-ad-client="ca-pub-XXXXXXXXXX"
      data-ad-slot={slot}
      data-ad-format={format}
      data-full-width-responsive="true"
    />
  );
}
実装理由
mountedフラグSSR時にwindowアクセスを防ぐ
adPushed refStrict Modeでの二重push防止
setTimeout(100)ハイドレーション完了を待つ
minHeight: 250CLS対策

ページ遷移対応

App Routerのソフトナビゲーションでは、コンポーネントが再マウントされない。keyで強制再マウント。

// components/AdWithPathKey.tsx
"use client";

import { usePathname } from "next/navigation";
import AdUnit from "@/components/AdUnit";

export default function AdWithPathKey({ slot }: { slot: string }) {
  const pathname = usePathname();
  return <AdUnit key={pathname} slot={slot} />;
}

keyが変わるとadPushedもリセットされ、新しい広告がpushされる。


Vercel特有の注意点

ads.txt の配置と確認

public/ads.txt に配置:

google.com, pub-XXXXXXXXXX, DIRECT, f08c47fec0942fa0

Vercelでの確認ポイント:

# デプロイ後に確認
curl -I https://your-domain.com/ads.txt

Content-Type: text/plain が返ること。200以外のステータスなら配置ミス。

クローラーのアクセス

Vercelはデフォルトでプレビューブランチに X-Robots-Tag: noindex を付ける。本番ドメインでは問題ないが、確認時に注意。

# ヘッダー確認
curl -I https://your-domain.com/ | grep -i robot

Edge Functionとの競合

Middleware でリダイレクトを挟むと、ads.txtへのアクセスが意図しない挙動になることがある。

// middleware.ts
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|ads.txt).*)'],
};

ads.txt をmatcherから除外。


CLS対策

広告読み込み時のレイアウトシフトはCore Web Vitalsに悪影響。

.ad-container {
  min-height: 250px;
}

@media (max-width: 768px) {
  .ad-container {
    min-height: 100px;
  }
}

広告枠の高さを事前確保。


開発環境での注意

localhostではAdSense広告は表示されない。これはAdSenseの仕様。

確認方法:

  • Vercelのプレビューデプロイを使う
  • 本番デプロイ後に確認

コンソールのadsbygoogle.push() errorは、本番では消えることが多い。


判断フローチャート

広告が表示されない
    ↓
localhost? → Yes → 本番/プレビューでテスト
    ↓ No
コンソールにエラー?
    ↓ Yes
「already been called」 → useRefフラグ未使用 → 広告コンポーネント修正
「No slot size」 → ins要素のサイズ未設定 → minHeight追加
    ↓ No
ads.txtは200返す? → No → public/配置確認、Vercel設定確認
    ↓ Yes
審査完了している? → No → 審査落ちパターン確認
    ↓ Yes
ページ遷移後に消える? → Yes → keyにpathname追加

トラブルシューティング

症状原因対処
広告が出ないlocalhost本番デプロイで確認
二重pushエラーuseRef未使用adPushedフラグを追加
遷移後に消える再マウントされないkeyにpathnameを追加
レイアウトシフト高さ未確保minHeightを設定
審査で落ちるクローラーがJS実行できないSSG/ISR活用、URL検査
ads.txt認識されないMiddleware競合matcherから除外

まとめ

  • スクリプトはlayout.tsxafterInteractiveで1回だけ
  • useRefで二重push防止(Strict Mode対策)
  • keyにpathnameを渡してページ遷移時に再マウント
  • minHeightでCLS対策
  • localhostでは広告が表示されない(仕様)
  • Vercelではads.txtとMiddlewareの競合に注意
  • 審査落ちはSSRレンダリングを疑う

関連記事