🥷

Next.jsで忍者AdMax広告が一つしか表示されない問題の解決方法

に公開

問題の概要

Next.jsのApp Routerを使用したSPA(Single Page Application)で忍者AdMaxの広告を複数ページに配置した際、以下の問題が発生しました。

現象

  • ページ遷移時に広告が表示されない

    • トップページで広告が表示されている状態で別のページに遷移すると、遷移先の広告が表示されない
  • ページをリロードすると正常に表示される

    • F5キーやブラウザの更新ボタンでページをリロードすると、広告が正常に表示される
  • URL直接変更では正常に動作する

    • ブラウザのアドレスバーでURLを手動で変更してページ遷移すると、広告が正常に表示される
  • 同一ページ内で複数の広告のうち一つしか表示されない

    • 同じページに複数の広告コンポーネントを配置しても、どれか一つしか表示されない

原因の分析

t.jsとは?

t.js忍者AdMaxの広告配信用コアスクリプトです。以下の役割を持っています:

主な機能

  1. 広告の初期化と配信

    • ページ上のwindow.admaxads配列を読み取る
    • data-admax-id属性を持つDOM要素を検索
    • 広告IDに基づいて適切な広告コンテンツを取得
    • DOM要素内に広告を挿入・表示
  2. 実行フロー

// ① ページ内で広告設定を配列に追加
(window.admaxads = window.admaxads || []).push({
  admax_id: "広告ID",
  type: "banner"
});

// ② t.jsスクリプトを読み込む
<script src="https://adm.shinobi.jp/st/t.js"></script>

// ③ t.jsが実行されると...
// - admaxads配列の内容を処理
// - 対応するDOM要素を探す
// - 広告サーバーと通信して広告を取得
// - DOM要素に広告を挿入

根本原因

このt.jsスクリプトは、通常のページ遷移(ページ全体のリロード)を前提とした設計になっています。

具体的には:

  1. スクリプトの初期化タイミング

    • t.jsは読み込まれた時点でwindow.admaxads配列を処理し、DOM上の広告要素を探して広告を挿入する
    • 処理後、配列を空にするか、既に処理済みのフラグを立てる
  2. Next.jsのクライアントサイドルーティング

    • Next.jsのApp Routerは、ページ遷移時にJavaScriptでコンテンツを差し替える(SPAナビゲーション)
    • ブラウザの完全なページリロードは発生しない
    • そのため、t.jsスクリプトが再実行されず、新しいページの広告が初期化されない
  3. グローバルスクリプトの再利用問題

    • 一度読み込まれたt.jsスクリプトはグローバルに存在し続ける
    • 新しいページでwindow.admaxads配列に広告設定を追加しても、スクリプトは既に初期化済みのため処理されない

試した方法と失敗した理由

❌ 方法1: PostScribeライブラリの使用

import PostScribe from '@marshallku/react-postscribe';

export default function NinjaAdMax() {
  const adHtml = `
    <div class="admax-ads" data-admax-id="xxx"></div>
    <script>(admaxads = window.admaxads || []).push({admax_id: "xxx", type: "banner"});</script>
    <script src="https://adm.shinobi.jp/st/t.js" async></script>
  `;

  return <PostScribe html={adHtml} />;
}

失敗理由: PostScribeはスクリプトを順次実行できるが、グローバルに一度読み込まれたt.jsは再実行されない

❌ 方法2: useEffectでスクリプトを動的に読み込み

useEffect(() => {
  const script = document.createElement('script');
  script.src = 'https://adm.shinobi.jp/st/t.js';
  document.body.appendChild(script);

  return () => {
    document.body.removeChild(script);
  };
}, []);

失敗理由: スクリプトタグの追加・削除はできるが、t.js自体が再初期化のメカニズムを持っていない

❌ 方法3: キャッシュバスティング

const script = document.createElement('script');
script.src = `https://adm.shinobi.jp/st/t.js?_=${Date.now()}`;

失敗理由: URLは変わるがスクリプト内部の処理ロジックは変わらないため、同じ問題が発生

解決方法:iframe方式

iframeとは?

iframe(inline frame)は、HTMLページ内に別のHTMLページを埋め込むための要素です。

iframeの特徴

  • 独立した環境: iframeは独自のwindowオブジェクトとdocumentオブジェクトを持つ
  • 完全な分離: 親ページのJavaScript、CSS、グローバル変数と干渉しない
  • サンドボックス化: セキュリティ上も分離された環境で実行される
<!-- 基本的な使い方 -->
<iframe src="https://example.com"></iframe>

<!-- インラインHTMLの埋め込み(今回使用する方法) -->
<iframe srcdoc="<!DOCTYPE html><html>...</html>"></iframe>

✅ 最終的な解決策

各広告を独立したiframe内に配置することで、完全に分離された環境で広告スクリプトを実行します。

実装コード

'use client';

import { Box } from '@mantine/core';
import { useEffect, useRef } from 'react';

export default function NinjaAdMaxSidebar() {
  const adMaxId = '対象のadMaxId';
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const container = containerRef.current;

    // 広告用のiframeを作成
    const iframe = document.createElement('iframe');
    iframe.style.cssText = 'width:160px;height:600px;border:none;display:block;margin:0 auto;';
    iframe.srcdoc = `
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <style>
          body { margin: 0; padding: 0; overflow: hidden; }
        </style>
      </head>
      <body>
        <div class="admax-ads" data-admax-id="${adMaxId}" style="display:inline-block;width:160px;height:600px;"></div>
        <script type="text/javascript">
          (window.admaxads = window.admaxads || []).push({admax_id: "${adMaxId}", type: "banner"});
        </script>
        <script type="text/javascript" charset="utf-8" src="https://adm.shinobi.jp/st/t.js" async></script>
      </body>
      </html>
    `;

    container.appendChild(iframe);

    return () => {
      container.innerHTML = '';
    };
  }, [adMaxId]);

  return (
    <Box
      ref={containerRef}
      style={{
        width: 160,
        height: 600,
        margin: '16px auto',
        textAlign: 'center',
      }}
    />
  );
}

なぜこの方法で解決するのか

1. 完全な環境分離

  • 各iframeは独立したwindowオブジェクトを持つ
  • 親ページのグローバル変数やスクリプトと干渉しない
  • 各iframeでt.jsが独立して実行される

2. 確実な初期化順序

  • iframe内でHTMLが読み込まれる際、以下の順序が保証される:
    1. 広告div要素の作成
    2. window.admaxads配列への追加
    3. t.jsスクリプトの読み込みと実行

3. クリーンアップの確実性

  • コンポーネントがアンマウントされる際、iframe全体が削除される
  • 次回マウント時は完全に新しいiframeが作成され、広告が再初期化される

4. SPAルーティングへの対応

  • ページ遷移時、古いページのコンポーネントがアンマウントされiframeが削除される
  • 新しいページのコンポーネントがマウントされ、新しいiframeが作成される
  • 各ページの広告が独立して動作する

まとめ

問題の本質

  • 忍者AdMaxは従来のMPA(Multi Page Application)を前提とした設計
  • Next.jsのようなSPAでは、ページ遷移時にスクリプトが再初期化されない

解決のポイント

  • iframe方式で各広告を完全に分離
  • 各iframeが独立したwindowコンテキストを持つ
  • ページ遷移時にiframeごと作り直すことで、確実に広告を再初期化

メリット

  • ✅ ページ遷移時も確実に広告が表示される
  • ✅ 同一ページ内に複数の広告を配置できる
  • ✅ クリーンアップが確実に行われる
  • ✅ 他のJavaScriptとの干渉がない

注意点

  • iframe内の広告はiframeのサイズに制約される
  • 広告のクリックイベントなどは親ページと分離される(通常は問題なし)
  • SEOの観点ではiframe内のコンテンツはインデックスされない(広告なので問題なし)

この方法は忍者AdMaxだけでなく、従来のページ遷移を前提とした他の広告サービスやスクリプトでも応用可能です。

参考情報

Discussion