Next.jsでバナー広告(AdStir)導入のトライ&エラー記録 ~SPAとdocument.writeの壁~
はじめに

ブログやメディアサイトを運営していると、広告ネットワークの導入は収益化の重要な一歩です。
Webエンジニアであれば何度も行ってきたであろう広告の導入で、今回も数時間で終わるだろうと思っていましたが、非同期処理ではまり、二日間を要しました。
今回は、Next.js(App Router構成)で日本の広告ネットワーク「AdStir」のバナー広告を導入しようとした際の、実装・トラブル・解決までの記録をまとめました。
この記事のポイント
- Next.js(SPA)で広告タグを埋め込む際のトライ&エラー
- CSP(Content Security Policy)との格闘
- Hydration Errorやdocument.writeの罠
- 最終的に「非同期タグ」が必要だった理由とその発見プロセス
最初の実装:公式タグをそのまま埋め込む
まずはAdStir管理画面で取得した「スマホ向けバナー広告(320x50)」の公式タグを、Reactコンポーネントに埋め込みました。
// ...existing code...
const adstirHtml = `
<script type="text/javascript">
var adstir_vars = {
ver: "4.0",
app_id: "MEDIA-XXXXXXX",
ad_spot: 1,
center: false
};
</script>
<script type="text/javascript" src="https://js.ad-stir.com/js/adstir.js"></script>
`;
return (
<div dangerouslySetInnerHTML={{ __html: adstirHtml }} />
);
// ...existing code...結果
→ 広告は表示されず、ブラウザのコンソールにエラーが出現。
Hydration Error(React error #418)との戦い
Next.js(App Router)はSSRとCSRのHTML差分に敏感です。広告タグの埋め込みで「Hydration mismatch」エラーが頻発。
試したこと
- Next.jsの`<Script>`コンポーネント(strategy: "afterInteractive")
- `useEffect`で動的に`script`タグを挿入
- dynamic import({ ssr: false })でクライアント専用化
import Script from 'next/script';
<>
<Script id="adstir-vars" strategy="afterInteractive">{`
var adstir_vars = { ver: "4.0", app_id: "MEDIA-XXXXXXX", ad_spot: 1, center: false };
`}</Script>
<Script id="adstir-src" strategy="afterInteractive" src="https://js.ad-stir.com/js/adstir.js" />
</>結果
→ Hydrationエラーは減ったが、広告は依然として表示されない。
CSP(Content Security Policy)との格闘
広告ネットワークは多くの外部ドメインを利用します。CSPでブロックされると、スクリプトやiframeが読み込まれません。
対応例(抜粋)
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.ad-stir.com https://*.ad-stir.com ...",
"frame-src 'self' https://*.ad-stir.com ...",
// ...他にもadsappier, pubmatic, rubiconproject, amoad, sp-trkなど都度追加
].join('; ');運用ポイント
- 広告表示のたびに新しいCSP警告が出る
- ブラウザのコンソールで警告を確認し、必要なドメインを追加
- ワイルドカード(*)は使えないので、都度手動で追加
document.writeの罠と非同期タグの必要性
最大の壁
AdStirの標準タグはdocument.write()を使って広告枠を生成します。
しかし、React/Next.js(SPA)では、document.write()は「非同期で挿入されたスクリプト」からは実行できません。
エラー例
A call to document.write() from an asynchronously-loaded external script was ignored.なぜ?
- Reactのレンダリング後に
scriptタグを挿入すると、ブラウザは「同期的なHTML解析中」ではないと判断 - そのため、
document.write()は無視され、広告枠が生成されない
試したこと
useEffectでscript.async = falseにしてみる- 変数定義→本体スクリプトの順で挿入
dangerouslySetInnerHTMLでインライン埋め込み
結果
→ どれもdocument.write()が無視され、広告は表示されない
非同期タグの発見と導入
らちが明かないとAdStirサポートに問い合わせたところ、「SPA対応の非同期タグ」が別途配布されていることが判明。
非同期タグの実装例
import { useEffect, useState, useRef } from 'react';
export default function AdStirBanner() {
const [isMobile, setIsMobile] = useState(false);
const scriptLoadedRef = useRef(false);
useEffect(() => {
const checkMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
setIsMobile(checkMobile);
if (checkMobile && !scriptLoadedRef.current) {
const script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src = '{非同期タグ向けのスクリプト}';
document.body.appendChild(script);
scriptLoadedRef.current = true;
}
}, []);
if (!isMobile) return null;
return (
<div style={{ height: '60px', maxWidth: '320px', margin: '0 auto' }}>
<div
//ここに広告SDKの情報を記載
/>
</div>
);結果
→ 非同期タグならdocument.write()を使わず、SPAでも広告が表示される!
!注意!
今回非同期タグの情報はこちらに記載していません。というのもAdstirに問い合わせしなくてはならないものなので、易々と公開していいものではないかと判断しました。
まとめと学び
- AdStirの標準タグ(document.write依存)はSPA/React/Next.jsでは動作しない
- CSPは広告ネットワークの追加ごとに都度対応が必要
- Hydration ErrorはSSR/CSRの差分が原因。dynamic importやクライアント限定描画で回避可能
- 最終的な解決策は「非同期タグ」の導入。ベンダーに問い合わせて入手するのが最短ルート
- ブラウザのコンソールでエラーを監視し、CSPや実装を都度調整する運用が現実的
Copilot活用ポイント
- 実装案の生成(Script, useEffect, dynamic import, CSPテンプレート)
- エラー原因の整理(Hydration mismatch, document.writeの仕様)
- デバッグ補助(ログ挿入、onload/onerrorハンドラ)
実装する時間が大幅に削減できいろいろなトライ&エラーを試すことができました。広告は過去に何度も導入した経験があり、そんなに時間がかからないだろうと思っていましたが、二日間要しました。しかし、仕事や練習の合間をぬった二日間で、Copilotを使っていなければ一週間以上かかっていたでしょう。
広告導入で困ったら、まずは「非同期タグがあるか」をベンダーに確認しましょう!
SPA/React/Next.jsでの広告実装は、ベンダーの仕様とブラウザの制約を理解することが最重要です。