SPA×GTMでページビュー計測を完全設定【2つの実装方法を徹底解説】

「SPAサイトにGTMを導入したのに、ページビューが計測されない…」とお悩みではありませんか?

Single Page Application(SPA)では、ページ遷移時にURLが変わってもブラウザのリロードが発生しないため、通常のGoogleタグマネージャー(GTM)設定では正確な計測ができません。この問題を解決しないと、GA4のデータが不正確になり、正しいマーケティング判断ができなくなってしまいます。

本記事では、SPAでGTMを正しく動作させる2つの実装方法を、初心者の方にもわかりやすく解説します。実装不要の「History Changeトリガー」と、確実性の高い「dataLayer.push」の両方を、具体的なコード例とともに紹介します。

この記事を読めば、React、Vue.js、Next.jsなど主要フレームワークでのGTM設定方法が理解でき、今日からすぐに実装できるようになります。


目次

SPAでGTMのページビュー計測が難しい理由

SPAでは従来のGTM設定が機能しない根本的な理由があります。

通常のサイトとSPAの違い

ページ遷移の仕組みが根本的に異なります。

通常のWebサイト(MPA)の場合

  • ユーザーがリンクをクリックすると、ブラウザが新しいHTMLファイルをサーバーから取得します
  • ページ全体がリロードされ、window.onloadイベントが発火します
  • GTMのデフォルト設定(Page Viewトリガー)は、このリロードを検知してタグを発火させます

SPAの場合

  • ページ遷移時もブラウザのリロードが発生しません
  • JavaScriptがDOM(Document Object Model)を動的に書き換えるだけです
  • URLは変わりますが、window.onloadイベントは発火しないため、GTMの通常設定では計測できません
項目通常のサイト(MPA)SPA
ページ遷移方法サーバーから新規HTML取得JavaScriptでDOM書き換え
ブラウザのリロード発生する発生しない
GTM標準設定での計測可能不可能
ユーザー体験ページ読込時に画面が白くなるスムーズな遷移

この違いにより、SPAでは特別なトリガー設定が必要になります。

SPAでタグが発火しない原因

SPAでGTMタグが発火しない原因は、GTMのデフォルト設定がブラウザのページロードイベントに依存しているためです。

具体的な問題点

  1. Page Viewトリガーの限界:GTMの標準「Page View – DOM Ready」トリガーは、初回ページ読み込み時のみ動作します。SPA内の仮想的なページ遷移は検知できません。
  2. URLの変化を検知できない:SPAはJavaScriptのhistory.pushState()replaceState()でURLを変更しますが、これらはブラウザのナビゲーションイベントを発火させません。
  3. タイミングの問題:SPAではコンテンツの描画タイミングが不定期なため、タグを発火させる適切なタイミングを特定するのが困難です。

実際に起こる問題

  • GA4でセッション数は計測されるが、ページビュー数が1のままになる
  • ランディングページのデータしか取得できない
  • コンバージョン計測が正確にできない
  • 広告タグが正しく動作せず、リマーケティングができない

これらの問題を解決するには、「History Changeトリガー」または「dataLayer.push」を使った特別な設定が必要です。次のセクションでこの2つの方法を詳しく解説します。


SPAでGTMを設定する2つの方法【メリット・デメリット比較】

SPAでGTMを正しく動作させる方法は、大きく分けて2つあります。それぞれの特徴を理解して、プロジェクトに最適な方法を選びましょう。

History Changeトリガーを使う方法

History ChangeトリガーはGTMが提供する組み込みトリガーで、history.pushState()replaceState()によるURL変更を自動検知します。

メリット

  • フロントエンド側の実装が不要で、GTM管理画面だけで完結します
  • 既存のSPAに後から追加する場合でも、開発チームに依頼せずマーケティング担当者だけで設定可能です
  • 導入のハードルが低く、テスト導入に最適です

デメリット

  • SPAの実装方式によっては正確に検知できない場合があります
  • ハッシュルーティング(URL末尾に#を使う方式)では動作しません
  • フレームワーク固有の仮想ルーティングに対応できないことがあります
  • タイミング制御が難しく、コンテンツ描画前にタグが発火する可能性があります

こんなプロジェクトに最適

  • まずは手軽に試してみたい場合
  • React Router、Vue Routerなど標準的なルーティングライブラリを使用している場合
  • マーケティング担当者が主導でGTM設定を行う場合

dataLayer.pushを使う方法

dataLayer.pushは、フロントエンド側で任意のタイミングでGTMにイベントを送信する方法です。

メリット

  • ページ遷移のタイミングを確実に捕捉できます
  • コンテンツが完全に描画された後にタグを発火させることが可能です
  • GA4以外の広告タグや分析ツールにも同じイベントを使い回せます
  • ページタイトルやカスタムディメンションなど、追加データを柔軟に渡せます
  • 長期的にメンテナンスしやすい安定した実装です

デメリット

  • フロントエンド開発者の協力が必要です
  • コードの修正とデプロイが必要になります
  • 実装の知識がない場合、学習コストがかかります

こんなプロジェクトに最適

  • 確実で精度の高い計測を行いたい場合
  • 複数の計測ツールを統合管理したい場合
  • エンジニアと協力して本格的に導入する場合
  • Next.js、Nuxt.jsなどのフレームワークを使用している場合

どちらを選ぶべきか?フレームワーク別の推奨設定

プロジェクトの状況とフレームワークに応じて、最適な方法を選択しましょう。

フレームワーク別の推奨方法

フレームワーク推奨方法理由
React(CRA/Vite)dataLayer.pushReact Routerのナビゲーションフックと相性が良い
Vue.js 3dataLayer.pushVue RouterのafterEachで確実に捕捉可能
Next.js(App Router)dataLayer.push必須サーバーコンポーネントとの整合性が取れる
Next.js(Pages Router)dataLayer.push推奨useEffectで実装が容易
Nuxt.jsdataLayer.push推奨ミドルウェアやプラグインで一元管理できる
AngularHistory Change可標準的なルーティングで動作する

判断基準のチェックリスト

以下の質問に答えて、最適な方法を選びましょう。

  1. 開発チームの協力が得られますか?
    • YES → dataLayer.push推奨
    • NO → History Change試行
  2. 計測の確実性をどれだけ重視しますか?
    • 高精度が必須 → dataLayer.push
    • まずは動作確認したい → History Change
  3. GA4以外のタグも管理しますか?
    • YES(広告タグ等も使う) → dataLayer.push
    • NO(GA4のみ) → どちらでも可
  4. 長期的に運用しますか?
    • YES → dataLayer.push
    • NO(短期テスト) → History Change

次のセクションから、それぞれの具体的な設定手順を解説します。


【方法1】History Changeトリガーの設定手順(実装不要)

History Changeトリガーを使えば、フロントエンド側の実装なしでSPAのページビュー計測が可能です。GTM管理画面だけで完結する設定方法を順番に解説します。

Step1: History Changeトリガーの作成

まずGTM管理画面でHistory Changeトリガーを新規作成します。

設定手順

  1. GTM管理画面にログインし、対象のコンテナを開きます
  2. 左メニューから「トリガー」をクリックし、「新規」ボタンを押します
  3. トリガー名を入力します(例:「SPA – History Change」)
  4. トリガーの設定画面で、右上の鉛筆アイコンをクリックします
  5. トリガーのタイプから「履歴の変更」を選択します
  6. 「このトリガーの発生場所」は、まず「すべての履歴変更イベント」を選択します
  7. 「保存」ボタンをクリックして完了です

初期設定のポイント 最初は条件を絞らずに「すべての履歴変更イベント」で設定することをおすすめします。これにより、どのようなイベントが発火しているかをプレビューモードで確認できます。確認後、必要に応じて条件を絞り込みます。

Step2: 組み込み変数の有効化(gtm.newUrl / gtm.oldUrl)

History Changeトリガーと連携する組み込み変数を有効化します。これらの変数により、遷移前後のURL情報を取得できます。

設定手順

  1. GTM管理画面の左メニューから「変数」をクリックします
  2. 「組み込み変数」セクションの「設定」ボタンを押します
  3. 以下の変数にチェックを入れて有効化します
    • History Source(履歴ソース)
    • New History Fragment(新しい履歴のフラグメント)
    • Old History Fragment(古い履歴のフラグメント)
    • New History State(新しい履歴の状態)
    • Old History State(古い履歴の状態)

特に重要な変数

  • New History URL Fragment(gtm.newUrlFragment):遷移後のURLを取得します
  • Old History URL Fragment(gtm.oldUrlFragment):遷移前のURLを取得します

これらの変数は、後でトリガー条件やGA4のパラメータ設定で使用します。

Step3: トリガー条件の絞り込み設定

全てのHistory Changeイベントを拾うと、意図しないタイミングでタグが発火する可能性があります。条件を絞り込んで、正しいページ遷移のみを計測しましょう。

基本的な絞り込み条件

作成したHistory Changeトリガーを編集し、「このトリガーの発生場所」を「一部の履歴変更イベント」に変更します。

よく使う条件設定例

  1. URLが実際に変わった時だけ発火させる New History Fragment - 等しくない - {{Old History Fragment}} この条件により、URLが本当に変更された場合のみタグが発火します。
  2. 特定のディレクトリ配下のみを対象にする New History Fragment - 含む - /app/ SPAが特定のパス配下でのみ動作する場合に有効です。
  3. 複数条件を組み合わせる New History Fragment - 等しくない - {{Old History Fragment}} かつ New History Fragment - 含まない - # URLの変更があり、かつハッシュ変更でない場合のみ発火します。

プレビューモードでの確認 条件設定後は必ずプレビューモードで動作確認を行いましょう。意図した遷移でトリガーが発火し、意図しない遷移では発火しないことを確認します。

Step4: GA4イベントタグの作成と紐付け

最後に、History ChangeトリガーでGA4のpage_viewイベントを送信するタグを作成します。

タグ設定手順

  1. GTM管理画面の「タグ」から「新規」をクリックします
  2. タグ名を入力します(例:「GA4 – SPA Page View」)
  3. タグの設定で「Google アナリティクス: GA4 イベント」を選択します
  4. 測定IDを入力します(GA4プロパティのG-から始まるID)
  5. イベント名に「page_view」と入力します

パラメータの設定 「イベントパラメータ」セクションで、以下を追加します。

パラメータ名
page_location{{New History Fragment}}
page_title{{Page Title}}

これにより、遷移後のURL情報がGA4に正しく送信されます。

トリガーの紐付け タグ設定画面の下部「トリガー」セクションで、先ほど作成した「SPA – History Change」トリガーを選択して保存します。

動作確認とデバッグ方法

設定が完了したら、必ず動作確認を行いましょう。

プレビューモードでの確認手順

  1. GTM管理画面右上の「プレビュー」ボタンをクリックします
  2. 対象サイトのURLを入力して「Connect」を押します
  3. サイト上でページ遷移を行います
  4. GTMデバッグパネルで「History Change」イベントが表示されることを確認します
  5. 「Tags Fired」に作成したGA4タグが表示されることを確認します

よくある問題と対処法

  • トリガーが発火しない:History Changeイベント自体が表示されない場合、SPAがhistory.pushState()を使用していない可能性があります
  • タグが2重に発火する:初回ページロード時のPage Viewトリガーと重複している可能性があります。条件で「Page URL – 等しい – 初回URL」を除外しましょう
  • URLが正しく取得できない:組み込み変数が有効化されているか再確認してください

確認が完了したら、「公開」ボタンでバージョンを作成し、本番環境に反映させましょう。


【方法2】dataLayer.pushを使った確実な計測方法

dataLayer.pushを使用すると、SPAのページ遷移を確実に捕捉できます。フロントエンド側で少しコードを追加する必要がありますが、最も安定した計測方法です。

フロント側の実装コード(コピペOK)

フロントエンド側では、ページ遷移が発生したタイミングでdataLayer.push()を実行します。以下のコードをコピーして使用できます。

基本的な実装コード

// ページ遷移時に実行する関数
function sendVirtualPageView(url, title) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'virtual_page_view',
    page_location: url || window.location.href,
    page_title: title || document.title,
    page_path: window.location.pathname
  });
}

// 使用例:ページ遷移時に呼び出す
sendVirtualPageView('/products', '商品一覧ページ');

実装のポイント

  1. dataLayerの初期化window.dataLayerが未定義の場合に備えて、window.dataLayer = window.dataLayer || [];で初期化します
  2. カスタムイベント名event: 'virtual_page_view'で独自のイベント名を定義します(GTM側でこの名前を使います)
  3. URL情報の送信page_location(完全URL)、page_path(パス)、page_title(タイトル)を送信します

デバッグ用のコード 開発環境では、以下のようにコンソールログを追加すると確認しやすくなります。

function sendVirtualPageView(url, title) {
  window.dataLayer = window.dataLayer || [];
  const data = {
    event: 'virtual_page_view',
    page_location: url || window.location.href,
    page_title: title || document.title,
    page_path: window.location.pathname
  };
  
  // 開発環境でのデバッグログ
  if (process.env.NODE_ENV === 'development') {
    console.log('GTM Virtual Page View:', data);
  }
  
  window.dataLayer.push(data);
}

このコードを各フレームワークのルーティング機能と統合することで、自動的にページビューを送信できます。

React/Vue/Next.js別の実装例

主要なJavaScriptフレームワークでの具体的な実装方法を解説します。

React(React Router使用)の実装

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    // ページ遷移時にGTMへ送信
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_title: document.title,
      page_path: location.pathname
    });
  }, [location]); // locationが変わるたびに実行

  return (
    // アプリのコンポーネント
  );
}

Vue.js 3(Vue Router使用)の実装

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ルート定義 */]
});

// ルート変更後に実行
router.afterEach((to, from) => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'virtual_page_view',
    page_location: window.location.href,
    page_title: to.meta.title || document.title,
    page_path: to.path
  });
});

export default router;

Next.js(App Router)の実装

// app/layout.js
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export default function RootLayout({ children }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + (searchParams.toString() ? `?${searchParams}` : '');
    
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_title: document.title,
      page_path: url
    });
  }, [pathname, searchParams]);

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

Nuxt.js 3の実装

// plugins/gtm.client.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:finish', () => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_title: document.title,
      page_path: window.location.pathname
    });
  });
});

各フレームワークのライフサイクルやルーティング機能に合わせて実装することで、確実にページ遷移を捕捉できます。

GTM側のカスタムイベントトリガー設定

フロント側で送信したvirtual_page_viewイベントを、GTM側で受け取るトリガーを作成します。

トリガー設定手順

  1. GTM管理画面の「トリガー」から「新規」をクリックします
  2. トリガー名を入力します(例:「カスタムイベント – Virtual Page View」)
  3. トリガータイプで「カスタム イベント」を選択します
  4. イベント名に「virtual_page_view」と入力します(フロント側のevent名と一致させる)
  5. 「このトリガーの発生場所」は「すべてのカスタム イベント」を選択します
  6. 保存ボタンをクリックして完了です

条件の絞り込み(オプション) 特定の条件でのみタグを発火させたい場合は、「一部のカスタム イベント」を選択し、条件を追加します。

例:特定のパス配下のみ

Page Path - 含む - /app/

このトリガーは、dataLayerにvirtual_page_viewイベントがpushされるたびに発火します。

データレイヤー変数の作成

フロント側から送信したページ情報(URL、タイトル等)をGTM側で受け取るため、データレイヤー変数を作成します。

変数作成手順

  1. page_location変数の作成
    • GTM管理画面の「変数」→「ユーザー定義変数」→「新規」をクリック
    • 変数名:「DL – page_location」
    • 変数タイプ:「データレイヤーの変数」
    • データレイヤーの変数名:「page_location」
    • 保存
  2. page_title変数の作成
    • 変数名:「DL – page_title」
    • 変数タイプ:「データレイヤーの変数」
    • データレイヤーの変数名:「page_title」
    • 保存
  3. page_path変数の作成
    • 変数名:「DL – page_path」
    • 変数タイプ:「データレイヤーの変数」
    • データレイヤーの変数名:「page_path」
    • 保存

命名規則のポイント 変数名の頭に「DL -」を付けることで、データレイヤー由来の変数であることが一目でわかります。これにより、他の変数と混同せず、チーム内での管理がしやすくなります。

GA4タグへのパラメータマッピング

最後に、作成した変数をGA4タグに紐付けて、ページビュー情報を送信します。

タグ設定手順

  1. GTM管理画面の「タグ」から「新規」をクリックします
  2. タグ名:「GA4 – Virtual Page View」
  3. タグタイプ:「Google アナリティクス: GA4 イベント」
  4. 測定ID:GA4プロパティのID(G-XXXXXXXXXX)を入力
  5. イベント名:「page_view」

イベントパラメータの設定 「イベントパラメータ」セクションで以下を追加します。

パラメータ名値(変数を選択)
page_location{{DL – page_location}}
page_title{{DL – page_title}}
page_path{{DL – page_path}}

トリガーの紐付け タグ設定画面下部の「トリガー」セクションで、先ほど作成した「カスタムイベント – Virtual Page View」トリガーを選択して保存します。

動作確認 プレビューモードで以下を確認します。

  1. ページ遷移時に「virtual_page_view」イベントが表示される
  2. 作成したGA4タグが発火する
  3. Variables タブで各データレイヤー変数に正しい値が入っている

確認が完了したら公開して、本番環境に反映させましょう。これでdataLayer.pushを使った確実なSPA計測が完了です。


SPAのGTM設定でよくあるトラブルと解決方法

SPAでGTMを設定する際には、いくつかの典型的な問題が発生します。ここでは実務でよく遭遇するトラブルと、その具体的な解決方法を解説します。

ページビューが2重に計測される場合

初回ページロード時とSPA遷移時の両方でタグが発火し、ページビューが重複してカウントされる問題がよく発生します。

原因 GTMの標準「Page View」トリガーとSPA用のトリガー(History Changeまたはカスタムイベント)が両方とも有効になっているためです。初回アクセス時に両方のトリガーが発火してしまいます。

解決方法1:標準Page Viewタグを初回のみに限定

既存のGA4設定タグのトリガーを編集します。

  1. 通常のGA4設定タグのトリガーを「初期化 – All Pages」のみに変更
  2. SPA用のPage Viewタグは別途作成し、History Changeまたはカスタムイベントトリガーのみを設定

解決方法2:条件で初回ロードを除外

SPA用トリガーに以下の条件を追加します。

Event - 等しくない - gtm.js

これにより、GTMコンテナ読み込み時のイベントを除外できます。

解決方法3:dataLayer.pushで統一

最も確実なのは、初回ロードも含めてすべてdataLayer.pushで管理する方法です。

// ページロード完了後に実行
window.addEventListener('load', () => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'virtual_page_view',
    page_location: window.location.href,
    page_title: document.title
  });
});

この場合、GA4設定タグは「コンテナの読み込み」トリガーで動作させ、Page Viewイベントは全てカスタムイベント経由で送信します。

URLが変わってもタグが発火しない場合

History Changeトリガーを設定したのに、ページ遷移時にタグが発火しない問題です。

原因と解決方法

原因1:SPAがhistory APIを使用していない 一部のSPAはhistory.pushState()ではなく、ハッシュルーティング(#/page形式)やカスタム実装を使用しています。

解決方法:

  • プレビューモードでHistory Changeイベント自体が表示されるか確認
  • 表示されない場合は、dataLayer.push方式への移行を検討
  • ハッシュルーティングの場合は後述の対処法を参照

原因2:トリガー条件が厳しすぎる URLのフィルター条件により、意図したページ遷移が除外されている可能性があります。

解決方法:

  • トリガー条件を一時的に「すべての履歴変更イベント」に戻す
  • プレビューモードでどのイベントが発生しているか確認
  • 必要最小限の条件に絞り込む

原因3:非同期ルーティングのタイミング問題 フレームワークによっては、URL変更とコンテンツ描画のタイミングにズレがあります。

解決方法:

// ルーティング完了後に明示的にpush
router.afterEach((to, from) => {
  // 少し遅延させてDOM更新を待つ
  setTimeout(() => {
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href
    });
  }, 100);
});

デバッグのチェックリスト

  1. ブラウザのコンソールでエラーが出ていないか確認
  2. GTMプレビューモードで「History Change」イベントが表示されるか確認
  3. dataLayer配列の内容をコンソールで確認:console.log(window.dataLayer)
  4. フレームワークのルーティングライブラリが正しく動作しているか確認

ハッシュルーティング(#)の場合の対処法

URLに#を使うハッシュルーティング(例:example.com/#/products)では、History Changeトリガーが動作しません。

なぜ動作しないか #以降の変更は、ブラウザの履歴には記録されますが、history.pushState()とは異なるメカニズムで動作するためです。

解決方法1:ハッシュ変更イベントを利用

フロント側でハッシュ変更を検知してdataLayerにpushします。

// ハッシュが変更されたら発火
window.addEventListener('hashchange', () => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'virtual_page_view',
    page_location: window.location.href,
    page_hash: window.location.hash,
    page_title: document.title
  });
});

// 初回ロード時も実行
window.addEventListener('load', () => {
  if (window.location.hash) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_hash: window.location.hash,
      page_title: document.title
    });
  }
});

解決方法2:History Hash ChangeトリガーをGTMで使用

GTMには「履歴のハッシュ変更」というトリガータイプがあります(バージョンによって利用可否が異なります)。

設定方法:

  1. トリガータイプで「履歴の変更」を選択
  2. 「New History Fragment」変数を使って条件設定
  3. GA4タグのpage_locationパラメータに「{{Page URL}}」変数を設定

解決方法3:History APIへの移行(推奨)

長期的には、ハッシュルーティングからHistory APIへの移行を検討することをおすすめします。React RouterやVue Routerは、設定変更だけでHistory モードに切り替えられます。

// React Router v6の例
import { createBrowserRouter } from 'react-router-dom';

// ハッシュモード(旧)
// createHashRouter([...])

// Historyモード(推奨)
createBrowserRouter([...])

プレビューモードで確認できない時の対策

GTMのプレビューモードが正常に動作せず、デバッグができない場合の対処法です。

問題1:プレビューモードが接続できない

解決方法:

  • ブラウザの広告ブロッカー(uBlock Origin、AdBlock等)を一時的に無効化
  • Cookieやローカルストレージがブロックされていないか確認
  • ブラウザのプライベートモード/シークレットモードを試す
  • 別のブラウザで試す(ChromeとFirefoxで挙動が異なることがあります)

問題2:SPAページ遷移後にプレビューが切れる

SPAの仕様により、ページ遷移後にGTMプレビューウィンドウとの接続が切れる場合があります。

解決方法:

  • ブラウザのコンソールで直接確認:console.log(window.dataLayer)
  • Google Analytics Debugger拡張機能を使用
  • GA4のDebugViewをリアルタイムで確認(GA4管理画面の「設定」→「DebugView」)

問題3:変数の値が表示されない

プレビューモードで変数タブを見ても、データレイヤー変数の値が表示されない場合があります。

解決方法:

  • イベントを選択した状態で「Variables」タブを確認(イベント発火前では値が取得できません)
  • データレイヤー変数の「データレイヤーのバージョン」を「バージョン2」に設定
  • コンソールでwindow.dataLayerの内容を直接確認し、正しくpushされているか検証

確実なデバッグ方法

最も確実なのは、以下のコードをフロント側に一時的に追加することです。

// dataLayer.push を監視
const originalPush = window.dataLayer.push;
window.dataLayer.push = function(...args) {
  console.log('dataLayer.push:', args);
  return originalPush.apply(window.dataLayer, args);
};

これにより、すべてのdataLayer.push呼び出しがコンソールに表示されます。デバッグ完了後は必ずこのコードを削除してください。


フレームワーク別の具体的な設定例

主要なJavaScriptフレームワークごとに、実務で使える具体的なGTM設定コードを紹介します。コピー&ペーストで使用できる実装例です。

React(Create React App / Vite)での設定

React RouterとuseEffectフックを使った実装方法です。

実装コード(App.jsまたはApp.tsx)

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    // GTMへページビューを送信
    const sendPageView = () => {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: 'virtual_page_view',
        page_location: window.location.href,
        page_path: location.pathname + location.search,
        page_title: document.title,
      });
    };

    // ページ遷移後、少し待ってから送信(タイトル更新を待つ)
    const timer = setTimeout(sendPageView, 100);
    
    return () => clearTimeout(timer);
  }, [location]);

  return (
    <Router>
      {/* アプリのコンポーネント */}
    </Router>
  );
}

カスタムフック化(再利用しやすい形)

// hooks/usePageTracking.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const usePageTracking = () => {
  const location = useLocation();

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_path: location.pathname + location.search,
      page_title: document.title,
    });
  }, [location]);
};

// App.jsで使用
import { usePageTracking } from './hooks/usePageTracking';

function App() {
  usePageTracking(); // これだけで自動計測

  return (
    <Router>
      {/* アプリのコンポーネント */}
    </Router>
  );
}

ポイント

  • useLocationでルーティング変更を検知
  • useEffectの依存配列にlocationを指定することで、遷移のたびに実行
  • クリーンアップ関数でタイマーをクリアし、メモリリークを防止

Vue.js / Nuxt.jsでの設定

Vue Router とナビゲーションガードを使った実装です。

Vue.js 3の実装(router/index.js)

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ルート定義
  ]
});

// ルーティング完了後に実行
router.afterEach((to, from) => {
  // 次のTick(DOM更新後)で実行
  setTimeout(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_path: to.fullPath,
      page_title: to.meta.title || document.title,
    });
  }, 100);
});

export default router;

Nuxt 3の実装(plugins/gtm.client.js)

export default defineNuxtPlugin((nuxtApp) => {
  // ページ遷移完了時のフック
  nuxtApp.hook('page:finish', () => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_path: window.location.pathname + window.location.search,
      page_title: document.title,
    });
  });

  // 初回ロード時も実行
  nuxtApp.hook('app:mounted', () => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_path: window.location.pathname + window.location.search,
      page_title: document.title,
    });
  });
});

ポイント

  • router.afterEachはナビゲーション完了後に実行される
  • Nuxtではpage:finishフックを使うのが最も確実
  • .client.jsサフィックスでクライアントサイドのみで実行

Next.js(App Router / Pages Router)での設定

Next.jsのバージョンとルーターの種類によって実装方法が異なります。

App Router(Next.js 13+)の実装

// app/layout.tsx
'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export default function RootLayout({ children }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + (searchParams.toString() ? `?${searchParams}` : '');
    
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'virtual_page_view',
      page_location: window.location.href,
      page_path: url,
      page_title: document.title,
    });
  }, [pathname, searchParams]);

  return (
    <html lang="ja">
      <head>
        {/* GTMスクリプト */}
      </head>
      <body>{children}</body>
    </html>
  );
}

Pages Router(Next.js 12以前)の実装

// pages/_app.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url: string) => {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: 'virtual_page_view',
        page_location: window.location.href,
        page_path: url,
        page_title: document.title,
      });
    };

    // ルート変更完了時にイベント発火
    router.events.on('routeChangeComplete', handleRouteChange);

    // 初回ロード時も実行
    handleRouteChange(router.asPath);

    // クリーンアップ
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events, router.asPath]);

  return <Component {...pageProps} />;
}

export default MyApp;

ポイント

  • App Routerでは'use client'ディレクティブが必須
  • Pages Routerではrouter.eventsでルーティングを監視
  • 初回ロード時の計測も忘れずに実装

これらの実装例をベースに、プロジェクトの要件に合わせてカスタマイズしてください。


GA4以外のタグ(広告タグ等)も同時に対応する方法

SPAで計測するのはGA4だけではありません。Google広告やMeta(Facebook)ピクセルなど、複数のタグを効率的に管理する方法を解説します。

Google広告のコンバージョンタグ

Google広告のコンバージョンタグもSPA対応が必要です。dataLayer.pushを使えば、GA4と同じトリガーで複数タグを管理できます。

実装方法

  1. フロント側でコンバージョンイベントを送信
// 購入完了ページなど
function sendConversion(transactionId, value) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'conversion',
    conversion_type: 'purchase',
    transaction_id: transactionId,
    value: value,
    currency: 'JPY'
  });
}

// 使用例
sendConversion('TXN-12345', 5000);
  1. GTM側でGoogle広告タグを作成
  • タグタイプ:「Google 広告のコンバージョン トラッキング」
  • コンバージョンID:広告管理画面から取得
  • コンバージョンラベル:広告管理画面から取得
  • コンバージョン値:{{DL - value}}(データレイヤー変数)
  • トリガー:カスタムイベント「conversion」
  1. 条件で分岐させる

複数種類のコンバージョンがある場合、変数で分岐できます。

トリガー条件:
Event - 等しい - conversion
かつ
DL - conversion_type - 等しい - purchase

Meta(Facebook)ピクセルの設定

Metaピクセルも同様にdataLayerと統合できます。

実装コード

  1. 基本設定(初回のみ実行)
// ページロード時に1度だけ実行
!function(f,b,e,v,n,t,s) {
  if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};
  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
  n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];
  s.parentNode.insertBefore(t,s)
}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');

fbq('init', 'あなたのピクセルID');
  1. SPAページビュー計測
// ページ遷移時
window.dataLayer.push({
  event: 'virtual_page_view',
  page_location: window.location.href
});

// 同時にMetaピクセルも発火
fbq('track', 'PageView');
  1. GTMでの設定(推奨)

GTMでMetaピクセルを管理する場合:

  • タグタイプ:「カスタムHTML」
  • HTML内容:
<script>
  fbq('track', 'PageView');
</script>
  • トリガー:カスタムイベント「virtual_page_view」

コンバージョンイベントの例

// 購入完了時
window.dataLayer.push({
  event: 'conversion',
  conversion_type: 'purchase',
  value: 5000,
  currency: 'JPY'
});

// Metaピクセル用のカスタムHTMLタグ
// <script>
//   fbq('track', 'Purchase', {
//     value: {{DL - value}},
//     currency: {{DL - currency}}
//   });
// </script>

複数タグを一元管理するベストプラクティス

複数の計測タグを効率的に管理するための設計パターンを紹介します。

1. イベント命名規則の統一

// 推奨:用途別に明確な命名
window.dataLayer.push({ event: 'virtual_page_view' });  // ページビュー
window.dataLayer.push({ event: 'user_login' });         // ログイン
window.dataLayer.push({ event: 'purchase' });           // 購入
window.dataLayer.push({ event: 'form_submit' });        // フォーム送信

// 非推奨:汎用的すぎる命名
window.dataLayer.push({ event: 'event1' });
window.dataLayer.push({ event: 'click' });

2. データ構造の標準化

全てのイベントで共通のデータ構造を使用します。

// 標準テンプレート
window.dataLayer.push({
  event: 'イベント名',
  event_category: 'カテゴリ',
  event_action: 'アクション',
  event_label: 'ラベル',
  event_value: 数値,
  // 追加データ
  user_id: 'ユーザーID',
  page_path: window.location.pathname
});

3. タグの発火順序を制御

複数タグを順番に発火させたい場合、GTMのタグシーケンス機能を使用します。

設定方法:

  1. メインタグの「詳細設定」→「タグの順序付け」を開く
  2. 「○○○の前にタグを配信」で先行タグを指定
  3. 「○○○の後にタグを配信」で後続タグを指定

例:GA4タグ → 広告タグ → カスタムタグの順に発火

4. 環境別の発火制御

本番環境のみでタグを発火させる設定例:

// データレイヤー変数「DL - environment」を作成
// 開発環境の判定
window.dataLayer.push({
  environment: window.location.hostname === 'localhost' ? 'dev' : 'production'
});

// GTMトリガー条件
// DL - environment - 等しい - production

5. エラーハンドリング

タグの実行エラーを監視する仕組みを追加します。

// グローバルエラーハンドラー
window.addEventListener('error', (event) => {
  if (event.filename && event.filename.includes('gtm')) {
    console.error('GTM Error:', event.message);
    // エラー通知サービスへ送信
  }
});

管理画面での整理

GTM内での整理方法:

  • フォルダ機能:タグを「GA4」「広告」「SNS」などのフォルダに分類
  • 命名規則:「[サービス名] – [用途] – [対象]」形式(例:「GA4 – Event – Purchase」)
  • メモ機能:各タグに設定意図や更新履歴をメモとして記録

これらのベストプラクティスに従うことで、タグが増えても管理しやすい状態を維持できます。


よくある質問

Q1: SPAでGTMを使う場合、追加費用は発生しますか?

いいえ、追加費用は一切発生しません。

Googleタグマネージャー(GTM)は無料で利用できるツールであり、SPAでの使用に際しても追加料金は発生しません。通常のWebサイトと同様に、無料でHistory ChangeトリガーやdataLayer機能を使用できます。

ただし、GTMとは別に、GA4やGoogle広告などの各計測ツール側での料金体系は確認が必要です。GA4は基本無料ですが、月間イベント数が1,000万を超える場合はGoogle Analytics 360(有料版)への移行が必要になることがあります。

Q2: dataLayer.pushとgtag()の違いは何ですか?

dataLayer.pushはGTM経由でタグを管理する方法、gtag()は直接GA4にデータを送信する方法です。

dataLayer.pushの特徴

  • GTMコンテナを通じて複数のタグ(GA4、広告タグ等)を一元管理できます
  • マーケティング担当者がGTM管理画面でタグを追加・変更できるため、開発者への依存度が下がります
  • データをいったんdataLayerに格納するため、GTM側で柔軟に加工・変換できます

gtag()の特徴

  • コード内で直接GA4に送信するため、GTMを経由しません
  • シンプルで軽量ですが、タグの追加・変更のたびにコード修正が必要です
  • 他の計測ツールを追加する場合、それぞれ個別に実装が必要です

SPAでは、将来的な拡張性を考慮してdataLayer.push + GTMの組み合わせを推奨します。

Q3: History Changeトリガーが動作しないフレームワークはありますか?

はい、一部のフレームワークや実装方式では正常に動作しない場合があります。

動作しないケース

  1. ハッシュルーティングを使用している場合:URL末尾に#を使う方式(例:example.com/#/page)では、History APIが使われないためHistory Changeトリガーは発火しません。
  2. カスタム実装のルーティング:独自のルーティングライブラリを使用している場合、history.pushState()を呼び出していない可能性があります。
  3. 古いバージョンのフレームワーク:Angular 1.x(AngularJS)やBackbone.jsなど、古いフレームワークではHistory APIをサポートしていないことがあります。

対処法 これらのケースでは、dataLayer.pushを使った実装に切り替えることで確実に計測できます。開発チームと協力してフロント側での実装を検討しましょう。

Q4: プレビューモードでは動作するのに本番では動かないのはなぜですか?

GTMコンテナを公開していない、またはキャッシュの問題が考えられます。

主な原因と解決方法

  1. GTMコンテナが未公開
    • プレビューモードで確認後、「送信」ボタンを押してバージョンを公開する必要があります
    • GTM管理画面右上の「公開」ボタンから最新バージョンが公開されているか確認してください
  2. ブラウザキャッシュの問題
    • ブラウザがGTMの古いバージョンをキャッシュしている可能性があります
    • スーパーリロード(Ctrl+Shift+R / Cmd+Shift+R)を試してください
    • シークレットモードで開いて動作確認してください
  3. CDNやプロキシのキャッシュ
    • CloudflareなどのCDNを使用している場合、GTMスクリプトがキャッシュされていることがあります
    • CDN管理画面からキャッシュをパージ(削除)してください
  4. 環境別の設定ミス
    • 開発環境と本番環境で異なるGTMコンテナIDを使用している場合、公開したコンテナが本番に反映されていない可能性があります
    • HTMLソース内のGTM-XXXXXXXが正しいコンテナIDか確認してください

通常は公開後15分程度で全世界に反映されますが、キャッシュの影響で最大1時間程度かかることもあります。

Q5: SPAで複数のGTMコンテナを使用できますか?

技術的には可能ですが、推奨されません。

複数コンテナを使用できるケース

  • 会社全体用のコンテナと部門別のコンテナを併用する場合
  • テスト用と本番用で異なるコンテナを使い分ける場合

実装例

<!-- 1つ目のコンテナ -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

<!-- 2つ目のコンテナ -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-YYYYYY');</script>

推奨されない理由

  1. dataLayerの競合:両方のコンテナが同じdataLayerを参照するため、イベントの重複や予期しない動作が発生する可能性があります
  2. パフォーマンス低下:GTMスクリプトが複数読み込まれるため、ページ読み込み速度が遅くなります
  3. 管理の複雑化:どのタグがどのコンテナで管理されているか把握が困難になります

推奨される代替案 複数チームでGTMを管理したい場合は、1つのコンテナ内で「ワークスペース」機能を活用し、変更を管理することを推奨します。GTMのワークスペースを使えば、複数の担当者が並行して作業でき、変更内容を統合できます。

Q6: SPAのページタイトルが正しく取得できません

JavaScriptでタイトルを動的に変更しているため、タイミングの問題が発生しています。

原因 多くのSPAフレームワークでは、ページ遷移時にURLが変わった直後にdocument.titleを書き換えます。GTMのトリガーがこのタイトル変更前に発火すると、古いタイトルが取得されてしまいます。

解決方法1:dataLayer.pushでタイトルを明示的に渡す

// フロント側でタイトルを明示的に指定
window.dataLayer.push({
  event: 'virtual_page_view',
  page_title: '新しいページのタイトル',  // 明示的に指定
  page_location: window.location.href
});

解決方法2:フレームワークのメタデータ機能を使う

多くのフレームワークにはページタイトルを管理する機能があります。

React Helmet(React)の例:

import { Helmet } from 'react-helmet-async';

function ProductPage() {
  return (
    <>
      <Helmet>
        <title>商品ページ</title>
      </Helmet>
      {/* ページコンテンツ */}
    </>
  );
}

解決方法3:GTM側で遅延させる

History Changeトリガーに100ms程度の遅延を追加します。

カスタムJavaScript変数を作成:

function() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(document.title);
    }, 100);
  });
}

ただし、この方法は確実性が低いため、できるだけフロント側で明示的にタイトルを渡す方法を推奨します。

Q7: GA4でリアルタイムレポートにページビューが表示されません

GTMの設定は正しくても、GA4側の設定に問題がある可能性があります。

確認すべきポイント

  1. 測定IDが正しいか確認
    • GTMのGA4タグに設定している測定ID(G-XXXXXXXXXX)が、対象のGA4プロパティのものか確認してください
    • GA4管理画面の「管理」→「データストリーム」で測定IDを確認できます
  2. データフィルタの確認
    • GA4で内部トラフィックフィルタが有効になっていると、自分のアクセスが除外されます
    • 「管理」→「データ設定」→「データフィルタ」を確認してください
  3. DebugViewで確認
    • GA4管理画面の「設定」→「DebugView」を開きます
    • GTMプレビューモードでサイトにアクセスすると、リアルタイムでイベントが表示されます
    • page_viewイベントが送信されているか確認してください
  4. イベント名の確認
    • GTMのGA4イベントタグで、イベント名が正確に「page_view」になっているか確認してください
    • スペースや大文字小文字の違いがあると計測されません
  5. ブラウザの拡張機能を無効化
    • 広告ブロッカーなどの拡張機能がGA4のリクエストをブロックしている可能性があります
    • シークレットモードで確認してください

データ反映のタイムラグ リアルタイムレポート以外の通常レポートは、24~48時間の遅延があります。設定直後はDebugViewとリアルタイムレポートで確認し、翌日以降に通常レポートで確認しましょう。


まとめ:SPA×GTMで確実にページビューを計測するには

本記事では、SPAでGoogleタグマネージャー(GTM)を使ってページビューを正確に計測する方法を解説しました。

重要なポイントの振り返り

  1. SPAでは特別な設定が必須 通常のWebサイトと異なり、SPAではページ遷移時にブラウザのリロードが発生しないため、GTMの標準設定では計測できません。History ChangeトリガーまたはdataLayer.pushを使った特別な設定が必要です。
  2. 2つの実装方法から選択
    • History Changeトリガー:フロント側の実装不要で手軽に試せますが、一部のSPA実装では動作しない場合があります
    • dataLayer.push:開発者の協力が必要ですが、最も確実で柔軟性の高い方法です
  3. フレームワークに合わせた実装 React、Vue.js、Next.js、Nuxt.jsなど、使用しているフレームワークのルーティング機能と統合することで、メンテナンスしやすい実装が可能です。
  4. 複数タグの一元管理 dataLayer.pushを使えば、GA4だけでなくGoogle広告やMetaピクセルなども同じイベントで管理でき、効率的な運用が実現します。

次のステップ

SPAでのGTM設定は、一度正しく実装すれば長期的に安定した計測が可能になります。まずはHistory Changeトリガーで動作確認を行い、必要に応じてdataLayer.push方式に移行することをおすすめします。

プレビューモードでの動作確認、GA4のDebugViewでのイベント確認を忘れずに行い、正確なデータ計測の基盤を構築しましょう。

関連記事

Googleタグマネージャー(GTM)の複数設置は可能?設定方法と注意点を完全解説

GTMでクリック計測を設定する方法【初心者向け完全ガイド】

【コピペOK】GTMイベント設定完全マニュアル|クリック・スクロール・フォーム送信を5分で実装

ブログ一覧に戻る

ご相談はこちら