Next.js(App Router)でAdSenseが動かない原因と対策
この記事が解決する状況
以下のどれかに当てはまるなら、この記事を読む価値がある。
| 状況 | 読むべきセクション |
|---|---|
| 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.jsonのincludeにこのファイルが含まれていることを確認。
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 ref | Strict Modeでの二重push防止 |
setTimeout(100) | ハイドレーション完了を待つ |
minHeight: 250 | CLS対策 |
ページ遷移対応
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.tsxにafterInteractiveで1回だけ useRefで二重push防止(Strict Mode対策)keyにpathnameを渡してページ遷移時に再マウントminHeightでCLS対策- localhostでは広告が表示されない(仕様)
- Vercelではads.txtとMiddlewareの競合に注意
- 審査落ちはSSRレンダリングを疑う
関連記事
- WP-Cronが動かない原因と対処 — 定期処理が動かない問題の切り分け
- DNSレコードの調べ方と判断基準 — ドメイン設定のトラブル解決