ゲスト
📢 PR Claude Opus 4.7 公開! 新機能・破壊的変更・移行手順を 5 分で確認 →

GTM の noscript iframe を React で書いたら GSC 認証が通らない件

2026-04-28

#GTM #Next.js #Static Export #GSC

Google Tag Manager(GTM)を Next.js 静的ビルドに導入して、Search Console(GSC)の所有権認証を GTM 経由で通そうとしたら、「Google Tag Manager が見つかりません」と弾かれ続ける事案にぶつかった。

GTM 公式ドキュメントは 2 箇所への配置を要求する:

  1. <head> 内に GTM の JavaScript スニペット
  2. <body> 直後に noscript iframe(JS 無効環境向け)

ハマったのは 2 番目の noscript iframe。React コンポーネントとして書くと、こうなる。

// 失敗例: app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <noscript>
          <iframe
            src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
            height="0"
            width="0"
            style={{ display: "none", visibility: "hidden" }}
          />
        </noscript>
        {children}
      </body>
    </html>
  );
}

これでビルドすると、出力 HTML はこうなる。

<body>
  <!--$--><noscript>...</noscript><!--/$-->
  <div id="__next">...</div>
</body>

問題は React が hydration マーカー(<!--$-->)を noscript の前に挿入すること。GTM の検証スクリプトは正規表現で「<body> の直後」を厳密に見ているため、間にコメントノードが挟まると 「直後」と判定されず認証失敗 になる。

さらに output: "export" で静的ビルドすると、JSX で書いた hidden div は dehydrated state を保持するための noise が周囲にまとわり付き、シンプルな iframe ですら GTM パーサが期待する位置に置かれない。

解決 は build 後の post-process で sed 相当の文字列置換。React に書かせるのを諦めて、ビルド完了後に全 HTML ファイルを舐めて <body> の直後に物理的に noscript iframe を埋め込む。

// scripts/inject-gtm-noscript.mjs
import fs from "node:fs/promises";
import path from "node:path";

const OUT_DIR = "out";
const GTM_NOSCRIPT =
  '<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>';

async function walkHtml(dir) {
  const out = [];
  async function walk(p) {
    const entries = await fs.readdir(p, { withFileTypes: true });
    for (const e of entries) {
      const full = path.join(p, e.name);
      if (e.isDirectory()) await walk(full);
      else if (e.isFile() && full.endsWith(".html")) out.push(full);
    }
  }
  await walk(dir);
  return out;
}

const htmlFiles = await walkHtml(OUT_DIR);
for (const f of htmlFiles) {
  let html = await fs.readFile(f, "utf8");
  // 既存の React 由来 noscript があれば除去(冪等性)
  html = html.replace(
    /<noscript><iframe src="https:\/\/www\.googletagmanager\.com\/ns\.html[^"]*"[^>]*><\/iframe><\/noscript>/g,
    ""
  );
  // <body ...> 直後に物理挿入
  const newHtml = html.replace(/(<body[^>]*>)/, `$1${GTM_NOSCRIPT}`);
  if (newHtml !== html) await fs.writeFile(f, newHtml, "utf8");
}

ポイントは 3 つ:

GSC の所有権認証は 正規表現の厳密さに依存 している。React の hydration コメントが間に入るだけで通らないので、JS で生成する経路は捨てて静的注入に倒すのが結局速い。

この手の「フレームワークが噛む noise」は Static Export を採用したときに必ず一度はぶつかる。GTM・Tag Manager 系・サードパーティスクリプトはどれも post-process injection を最初から組み込んでおくのが安全だと学んだ。