広告計測について

広告効果の計測は時間がかかりサイト全体に影響があるので、
安易に考えないでほしい内容です。
ここに最低限の対応を記載してみます。

今回の目的

「広告クリック → コンバージョン」までのデータ取得ができる状態にすること

広告効果を測定するための「パラメータ付きURL」

1. そもそもパラメータ付きURLとは?

広告やSNS投稿などのURLの末尾に「utm_campaign」などの計測用パラメータを追加することで、Google Analytics(GA)などのツールで「どの広告から来たアクセスか?」を分析できるようになります。

パラメータ付きURLの作成手順(Campaign URL Builder 使用)

〇 使用ツール

Googleの公式ツール「Campaign URL Builder」
※GA4にも対応済み

📌 入力項目(主要パラメータ)

パラメータ説明
Website URL(必須)訪問先のページURLhttps://example.com/landing
Campaign Source(必須)トラフィックの出所google, facebook, newsletter など
Campaign Medium(必須)媒体cpc(広告), email, banner など
Campaign Nameキャンペーン名summer_sale, 2025_launch など
Campaign Termキーワード(任意)広告のキーワード(PPC広告時)
Campaign Content広告の内容(任意)A/Bテスト用のバナー名など

URLを使った広告効果の確認方法(Google Analytics で)

  1. 上記URLを使って広告を出す(例:Google広告に設定)。
  2. GA4にアクセス。
  3. 「集客 > トラフィック獲得」などのレポートで
    • utm_source → どこから来たか
    • utm_medium → どの媒体か
    • utm_campaign → どのキャンペーンか
      などが確認できます。

運用のポイント

  • 一貫性:utmの値(sourceやcampaign名など)は統一ルールを作ると後の分析がしやすくなります。
  • 記録:チームで共有するスプレッドシートなどに発行したURLとパラメータ内容を記録しておきましょう。
  • 文字数:あまり長くならないように各パラメータは15文字程度に抑える
  • 短縮は使わない:URLが長くなる場合でも、URL短縮サービス(bitlyなど)は推奨しません。理由は、後続ページでのUTMパラメータの引き継ぎや計測が煩雑になり、GA上での分析精度が下がる可能性があるためです。

コンバージョン計測

GoogleのUTMパラメータを使えば「どの広告から流入したか」はわかりますが、コンバージョン(例:資料請求、購入、問い合わせ)まで正確にひもづけるには追加の工夫が必要です。

下記のスクリプトの役割です
このスクリプトを使うことで:
GAやCRMでユーザーの行動分析や絞り込みがしやすくなる
広告→流入→フォーム送信→コンバージョンまで、一貫してトラッキング可能
「広告の費用対効果(ROAS)」を正確に測定

計測タグを引き継ぐJS

/**
 * utm_url_val_add_v9.js
 * ©2025 sakaida.jp WEBディレクターって なんですの?
 * - UTMパラメータをセッションに保持し、<a>, <form>, <button>, <input> に付加
 * - form要素ではactionを書き換えず、hidden inputで追加
 * - POST送信後の画面ではhiddenからUTMを読み出してURLに追加(アンカー含め保持)
 */

(function () {
  document.addEventListener('DOMContentLoaded', function () {
    const PARAM_KEYS = [
      'utm_source',
      'utm_medium',
      'utm_campaign',
      'utm_term',
      'utm_content'
    ];
    const STORAGE_KEY = '__myUtmParams__';

    // 現在のURLの検索パラメータを取得
    const url = new URL(window.location.href);
    const rawParams = url.searchParams;

    let safeParams = {};

    // 危険な値でないかチェック
    function isSafe(val) {
      return typeof val === 'string' && !/[<>'"(){}]|script|on\w+=|javascript:|data:/i.test(val);
    }

    // 不要な文字を除去
    function sanitize(val) {
      return val.replace(/[^\w\-]/g, '');
    }

    // UTMを抽出・サニタイズ
    PARAM_KEYS.forEach(k => {
      const val = rawParams.get(k);
      if (val && isSafe(val)) safeParams[k] = sanitize(val);
    });

    // セッションストレージに保存(既に保存済みなら読み出し)
    const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
    if (Object.keys(stored).length === 0 && Object.keys(safeParams).length > 0) {
      sessionStorage.setItem(STORAGE_KEY, JSON.stringify(safeParams));
    } else if (Object.keys(stored).length > 0) {
      safeParams = stored;
    }

    // 要素にUTMを追加
    function addParamsToElement(element, type) {
      try {
        let targetUrlString = '';

        if (type === 'a' && element.hasAttribute('href')) {
          targetUrlString = element.getAttribute('href');
        } else if (type === 'form' && element.hasAttribute('action')) {
          targetUrlString = element.getAttribute('action');
        } else if ((type === 'button' || type === 'input') && element.hasAttribute('formaction')) {
          targetUrlString = element.getAttribute('formaction');
        } else {
          return;
        }

        let isHashOnly = false;

        // href="#xxx" のような場合
        if (type === 'a' && targetUrlString.startsWith('#')) {
          targetUrlString = location.pathname + location.search + targetUrlString;
          isHashOnly = true;
        }

        const targetUrl = new URL(targetUrlString, location.origin);
        const originalHash = targetUrl.hash;
        let modified = false;

        Object.keys(safeParams).forEach(k => {
          targetUrl.searchParams.set(k, safeParams[k]);
          modified = true;
        });

        if (!modified) return;

        if (type === 'a') {
          targetUrl.hash = originalHash;
          element.href = isHashOnly
            ? targetUrl.pathname + targetUrl.search + originalHash
            : targetUrl.href;
        } else if (type === 'form') {
          Object.keys(safeParams).forEach(k => {
            const input = document.createElement('input');
            input.type = 'hidden';
            input.name = k;
            input.value = safeParams[k];
            element.appendChild(input);
          });
          if (element.method.toLowerCase() === 'get') {
            element.method = 'post';
          }
        } else if (type === 'button' || type === 'input') {
          const targetForm = element.form;
          if (targetForm) {
            const actionUrl = new URL(element.getAttribute('formaction'), location.origin);
            const originalHash = actionUrl.hash;

            Object.keys(safeParams).forEach(k => {
              actionUrl.searchParams.set(k, safeParams[k]);
            });
            actionUrl.hash = originalHash;
            element.setAttribute('formaction', actionUrl.href);

            Object.keys(safeParams).forEach(k => {
              const input = document.createElement('input');
              input.type = 'hidden';
              input.name = k;
              input.value = safeParams[k];
              targetForm.appendChild(input);
            });

            if (targetForm.method.toLowerCase() === 'get') {
              targetForm.method = 'post';
            }
          } else {
            element.setAttribute('formaction', targetUrl.href);
          }
        }
      } catch (e) {
        // 無効なURLなどのエラーを無視
      }
    }

    // 初期ロード時にすべての対象要素に対してUTM付加
    function processAllElements() {
      Array.from(document.getElementsByTagName('a')).forEach(el => addParamsToElement(el, 'a'));
      Array.from(document.getElementsByTagName('form')).forEach(el => addParamsToElement(el, 'form'));
      Array.from(document.querySelectorAll('button[formaction]')).forEach(el => addParamsToElement(el, 'button'));
      Array.from(document.querySelectorAll('input[type="submit"][formaction]')).forEach(el => addParamsToElement(el, 'input'));
    }

    // 動的に追加された要素にも対応
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType !== 1) return;

          if (node.tagName === 'A') addParamsToElement(node, 'a');
          if (node.tagName === 'FORM') addParamsToElement(node, 'form');
          if (node.tagName === 'BUTTON' && node.hasAttribute('formaction')) addParamsToElement(node, 'button');
          if (node.tagName === 'INPUT' && node.type === 'submit' && node.hasAttribute('formaction')) addParamsToElement(node, 'input');

          if (node.querySelectorAll) {
            node.querySelectorAll('a, form, button[formaction], input[type="submit"][formaction]')
              .forEach(el => {
                const tag = el.tagName;
                if (tag === 'A') addParamsToElement(el, 'a');
                if (tag === 'FORM') addParamsToElement(el, 'form');
                if (tag === 'BUTTON') addParamsToElement(el, 'button');
                if (tag === 'INPUT') addParamsToElement(el, 'input');
              });
          }
        });
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });

    processAllElements();

    // --- POST送信後のページで、hiddenからUTMを復元しURLに追加 ---
    const currentUrl = new URL(window.location.href);
    const utmFromHidden = {};

    PARAM_KEYS.forEach(k => {
      const el = document.querySelector(`input[name="${k}"]`);
      if (el && el.value) utmFromHidden[k] = el.value;
    });

    if (Object.keys(utmFromHidden).length > 0) {
      Object.keys(utmFromHidden).forEach(k => {
        currentUrl.searchParams.set(k, utmFromHidden[k]);
      });

      // アンカーも含めて正しく再構成
      const newUrl = currentUrl.origin + currentUrl.pathname + '?' + currentUrl.searchParams.toString() + currentUrl.hash;
      history.replaceState(null, '', newUrl);
    }
  });
})();

実装方法

  1. 上記スクリプト(utm_url_val_add_v9.js)を任意のファイルとして保存
  2. HTMLの<head>などに下記のように読み込み
<script src="/path/to/utm_url_val_add_v9.js" defer></script>

セキュリティ対応も万全

  • JavaScript内でパラメータの値に対して XSS対策(文字フィルタリング) がされている
  • script, onload, data: などの危険なパターンを排除
  • sanitize() により不要な文字も除去

HTML側での設定:コンバージョンデータを「わかりやすく」するために

GAでのコンバージョン分析を分かりやすくするために、HTML側で以下の対応が重要です:

A. ページの <title> を適切に設定

例(サンクスページ):

<title>資料請求完了|〇〇株式会社</title>

GAでは page_title として収集されるので、何のページでのコンバージョンかがレポート上明確になります。

B. <h1> も明確に

ユーザーにも検索エンジンにも意味が伝わるように:

<h1>資料請求ありがとうございました</h1>

C. メタタグで補足情報

例:

<meta name="robots" content="noindex, nofollow">
<meta name="description" content="〇〇株式会社への資料請求が完了しました。">

※「サンクスページ」をインデックスさせたくない場合に便利。

D. hiddenで送信されたUTMを確認用に出力(開発・検証用)

フォーム送信後に送られたパラメータを、GET や hidden で受け取って可視化すると確認が簡単です:

<script>
  const params = new URLSearchParams(location.search);
  console.log("UTMパラメータ:", Object.fromEntries(params.entries()));
</script>

フォーム送信先で受け取れるようにする

フォーム送信時(POST/GET)に、UTM情報が hidden フィールドとして送られていれば、バックエンドやCRM側でも「どの広告経由か?」が記録可能です。

<!-- 例:受信側でPHPなどで記録可能 -->
<input type="hidden" name="utm_source" value="google">
<input type="hidden" name="utm_campaign" value="spring_sale">

今回までの成果(準備段階)

内容状態
UTMの取得取得してセッションに保存
ページ遷移での保持 自動でリンクやフォームに付加
フォーム送信 hiddenによりUTMも送信される
HTML側での視認性 title/h1/metaでコンバージョン明示

どんな内容で何を計測したいか?
どんなレポートが欲しいかによって準備内容が異なります。

今回はここまで・・・・ではまた