📘 HubSpot CMS 組み込み構築 教科書
Chapter 7

フォーム・CTA・コンバージョン設計

HubSpotフォームの2つの実装方法・CSS上書きパターン・コンテンツ種別連動フォーム切り替え・CTAの設計・サンクスページ・GA4/GTMによるコンバージョン計測まで、コンバージョン設計の全体像を解説します。

🎯 対象レベル:中級〜上級
⏱ 読了目安:60〜90分
🔗 前章:第6章 データ活用・動的コンテンツ
🆕 2026年3月版 — 旧CTAエディター廃止(2025年11月)対応

この章の内容

  1. HubSpotフォームの概要と2つの実装方式
  2. HubL form タグによるネイティブ実装
  3. JavaScript(hbspt.forms.create)による実装
  4. フォームCSSの上書き完全ガイド
  5. コンテンツ種別に応じたフォーム自動切り替え
  6. マルチステップフォームとポップアップフォーム
  7. サンクスページとリダイレクト設計
  8. CTAの設計と実装パターン
  9. コンバージョン計測(GA4 / GTM連携)
  10. 第7章まとめ
Section 7-1

HubSpotフォームの概要と2つの実装方式

HubSpotフォームはCRMと直結しており、送信データがそのままコンタクトレコードに反映されます。 テンプレートへの組み込み方には2つの方式があり、用途に応じて使い分けます。

HubL form タグ方式

  • HubLテンプレートに直接記述
  • サーバーサイドでレンダリング
  • JSなしで動作(フォームのHTML直接出力)
  • シンプルな実装・テンプレートに固定配置
  • リダイレクトURLをHubLから渡せる
  • インラインCSSが多い(上書きに工夫が必要)

JS hbspt.forms.create 方式

  • JavaScriptでDOMにフォームを動的生成
  • クライアントサイドでレンダリング
  • フォームIDをJSで動的に切り替えられる
  • コンテンツ種別連動切り替えに最適
  • 送信後コールバック(onFormSubmit)が使える
  • 外部スクリプトの読み込みが必要

フォームIDの確認方法

HubSpotのフォームIDは管理画面の 「マーケティング → フォーム」からフォームを選択し、 URLの app.hubspot.com/forms/PORTAL_ID/FORM_ID/edit で確認できます。またはフォームの「共有」タブから埋め込みコードを確認すると記載されています。

用途推奨実装方式理由
固定ページのフォームセクションHubL form タグテンプレートに直接記述でき、シンプル
コンテンツ種別でフォームを切り替えるJS hbspt.forms.createフォームIDをHubL変数から受け取って動的に変更できる
モーダル・ポップアップフォームJS hbspt.forms.createDOM操作と組み合わせやすい
送信後に独自処理をしたいJS hbspt.forms.createonFormSubmit コールバックが使える
ランディングページどちらでも可要件に応じて選択

Section 7-2

HubL form タグによるネイティブ実装

HubLの {% form %} タグは最もシンプルなフォーム実装方法です。 サーバーサイドでフォームのHTMLが生成されるため、 JavaScriptが無効な環境でも動作します。

HubL — form タグの基本実装
{# 最もシンプルな実装 #}
{% form
  form_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
%}
HubL — form タグの全オプション
{% form
  {# ===== 必須 ===== #}
  form_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

  {# ===== リダイレクト設定 ===== #}
  response_redirect="https://example.com/thanks"
  {# 送信後のリダイレクトURL。未指定ならHubSpot管理画面の設定が使われる #}

  {# ===== サンクスメッセージ(リダイレクトしない場合)===== #}
  response_message="<p>送信ありがとうございます。担当者よりご連絡します。</p>"

  {# ===== HubSpot Cookieをフォームに紐付ける(トラッキング精度向上)===== #}
  submit_button_text="送信する"
  {# ボタンテキストをHubLから上書きできる #}

  {# ===== 事前入力(hidden フィールド)===== #}
  form_field_values_json='{"lifecyclestage": "lead", "hs_lead_status": "NEW"}'
  {# フォーム送信時にCRMプロパティを自動セットする #}
%}

form タグをモジュールから使う

form-section.module / module.html
{# fields.json で form フィールドと redirect_url フィールドを定義しておく #}

<section class="form-section">
  {% if module.section_title %}
    <h2 class="form-section__title">{{ module.section_title }}</h2>
  {% endif %}
  {% if module.section_desc %}
    <p class="form-section__desc">{{ module.section_desc }}</p>
  {% endif %}

  {% if module.form and module.form.form_id %}
    <div class="form-section__body">
      {% form
        form_id="{{ module.form.form_id }}"
        response_redirect="{{ module.redirect_url|default("") }}"
      %}
    </div>
  {% else %}
    <p class="form-section__notice">
      フォームが設定されていません。モジュールの設定からフォームを選択してください。
    </p>
  {% endif %}
</section>

隠しフィールドでコンテキスト情報を自動セット

フォーム送信時にページのコンテキスト情報(どのページから来たか、どのコンテンツ種別か等)を CRMに自動記録することで、営業・マーケターへの引き渡し品質が上がります。

HubL — form_field_values_json でコンテキストを自動セット
{# コンテンツ種別を判定しておく(第3章・第6章のタグ判定パターン)#}
{% set ns = namespace(content_type="blog") %}
{% for tag in content.tag_list %}
  {% if tag.slug starts_with "type:" %}
    {% set ns.content_type = tag.slug|replace("type:","") %}
  {% endif %}
{% endfor %}

{# フォームに hidden フィールドとして渡す #}
{% form
  form_id="YOUR_FORM_ID"
  form_field_values_json='{
    "hs_lead_status"         : "NEW",
    "content_type_custom__c" : "{{ ns.content_type }}",
    "first_conversion_page"  : "{{ content.absolute_url }}",
    "first_conversion_title" : "{{ content.name|escape }}"
  }'
%}
⚠️ form_field_values_json はフォームに存在するフィールドのみ有効

form_field_values_json で渡せるのは、そのフォームに設定されたフィールド(hidden フィールドを含む)のみです。 フォーム側に対応するフィールドが存在しないプロパティ名を指定しても無視されます。 CRMのカスタムプロパティに値を渡したい場合は、フォームにhiddenフィールドとして追加してから指定してください。


Section 7-3

JavaScript(hbspt.forms.create)による実装

hbspt.forms.create() はHubSpotが提供するJavaScript APIで、 任意のDOM要素にフォームを動的に生成します。 フォームIDをJSの実行時に決定できるため、 コンテンツ種別・ユーザー属性に応じた動的なフォーム切り替えに最適です。

基本実装

HubL + JS — hbspt.forms.create 基本実装
{# HTML:フォームの挿入先要素 #}
<div id="hs-form-target"></div>

{# JS:フォームの生成 #}
<script src="//js.hsforms.net/forms/embed/v2.js"></script>
<script>
  hbspt.forms.create({
    // ===== 必須パラメータ =====
    region  : "na1",   // リージョン(日本は通常 na1)
    portalId: "{{ hub_id }}",   // ポータルID(HubL変数から取得)
    formId  : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",

    // ===== 挿入先のCSSセレクタ =====
    target  : "#hs-form-target",

    // ===== 送信後リダイレクト =====
    redirectUrl: "https://example.com/thanks",

    // ===== 送信後コールバック =====
    onFormSubmit: function($form) {
      // 送信直後(リダイレクト前)に実行される
      console.log('フォーム送信完了', $form);
    },

    // ===== 送信完了後コールバック =====
    onFormSubmitted: function($form, data) {
      // サンクスメッセージ表示後に実行される
      // GA4へのコンバージョン送信などに使う
      if (window.gtag) {
        gtag('event', 'generate_lead', {
          event_category: 'form',
          event_label    : document.title
        });
      }
    },

    // ===== フォームの準備完了コールバック =====
    onFormReady: function($form) {
      // フォームのDOMが生成された直後に実行される
      // 入力欄へのfocusなどの初期処理に使う
    }
  });
</script>

HubL 変数をJSに渡してフォームIDを動的決定する

blog-post.html — コンテンツ種別でフォームを切り替える完全実装
{# ① HubL でコンテンツ種別を判定 #}
{% set ns = namespace(content_type="blog") %}
{% for tag in content.tag_list %}
  {% if tag.slug starts_with "type:" %}
    {% set ns.content_type = tag.slug|replace("type:","") %}
  {% endif %}
{% endfor %}

{# ② フォームID辞書と設定をHubLで定義 #}
{% set form_config = {
  "blog"       : {
    "id"      : "BLOG_FORM_ID",
    "title"   : "ご相談・お問い合わせ",
    "desc"    : "記事の内容についてご質問があればお気軽にどうぞ",
    "redirect": "/thanks/contact"
  },
  "seminar"    : {
    "id"      : "SEMINAR_FORM_ID",
    "title"   : "セミナーに申し込む",
    "desc"    : "お申込み後、確認メールをお送りします",
    "redirect": "/thanks/seminar"
  },
  "whitepaper" : {
    "id"      : "WHITEPAPER_FORM_ID",
    "title"   : "資料を無料ダウンロード",
    "desc"    : "フォームにご入力後、すぐにダウンロードできます",
    "redirect": "/thanks/whitepaper"
  },
  "case"       : {
    "id"      : "CASE_FORM_ID",
    "title"   : "事例資料を請求する",
    "desc"    : "詳細資料をPDFでお送りします",
    "redirect": "/thanks/case"
  }
} %}

{% set fc = form_config[ns.content_type]|default(form_config["blog"]) %}

{# ③ HTMLセクション #}
<section class="post-form-area post-form-area--{{ ns.content_type }}">
  <h2 class="post-form-area__title">{{ fc.title }}</h2>
  <p  class="post-form-area__desc">{{ fc.desc }}</p>
  <div id="hs-post-form"></div>
</section>

{# ④ HubL変数 → JS に橋渡し → フォーム生成 #}
<script src="//js.hsforms.net/forms/embed/v2.js"></script>
<script>
  var _hsFormConfig = {
    portalId   : "{{ hub_id }}",
    formId     : "{{ fc.id }}",
    redirectUrl: "{{ fc.redirect }}",
    contentType: "{{ ns.content_type }}",
    pageTitle  : "{{ content.name|escape }}",
    pageUrl    : "{{ content.absolute_url }}"
  };

  hbspt.forms.create({
    region     : 'na1',
    portalId   : _hsFormConfig.portalId,
    formId     : _hsFormConfig.formId,
    target     : '#hs-post-form',
    redirectUrl: _hsFormConfig.redirectUrl,

    // 送信時にGTMイベントを発火
    onFormSubmitted: function() {
      if (window.dataLayer) {
        window.dataLayer.push({
          event       : 'hs_form_submit',
          contentType : _hsFormConfig.contentType,
          formTitle   : _hsFormConfig.pageTitle,
          formUrl     : _hsFormConfig.pageUrl
        });
      }
    }
  });
</script>

Section 7-4

フォームCSSの上書き完全ガイド

HubSpotフォームはデフォルトのスタイルを持っており、サイトのデザインと合わせるためには CSSの上書きが必要です。ただし、HubSpotフォームはインラインスタイルや !important を使った強いスタイルが多く、 上書きには優先度の高いセレクタを使う必要があります。

CSSの適用レイヤー(上書き優先順位)

4
サイトのCSS(最優先)
↑ ここで上書き
3
HubSpotテーマのデフォルトスタイル
theme.json / module.css
2
HubSpotフォームのデフォルトCSS
HubSpotが自動挿入するベーススタイル
1
インラインスタイル(最低優先)→ !important で上書き要
一部要素にインラインが付く場合がある

HubSpotフォームのDOM構造

HubSpotフォームが生成するHTML構造(抜粋)
<div class="hbspt-form">         <!-- フォーム全体ラッパー -->
  <form class="hs-form">        <!-- form タグ本体 -->

    <div class="hs-form-field">   <!-- 各フィールドのラッパー -->
      <label>お名前</label>
      <div class="hs-input">        <!-- 入力要素ラッパー -->
        <input type="text" class="hs-input">
      </div>
      <ul class="hs-error-msgs">     <!-- バリデーションエラーメッセージ -->
        <li><label class="hs-error-msg">必須項目です</label></li>
      </ul>
    </div>

    <div class="hs-field-desc">    <!-- フィールドの説明文 -->
    <fieldset class="form-columns-2"> <!-- 2カラムレイアウト時 -->

    <div class="hs-submit">        <!-- 送信ボタンエリア -->
      <input type="submit" class="hs-button primary large">
    </div>

    <div class="hs-richtext">      <!-- フォームのリッチテキスト説明文 -->
    <div class="submitted-message"> <!-- 送信後のサンクスメッセージ -->

  </form>
</div>

フォームCSSの上書き実装例

css/form-overrides.css — HubSpotフォームのスタイル上書き
/* ===== HubSpot フォーム スタイル上書き =====
   セレクタの優先度を高めるために .post-form-area 等の
   親クラスで囲んでスコープを限定する
===================================================== */

/* フォーム全体のリセット */
.post-form-area .hs-form,
.form-section .hs-form {
  max-width: 100%;
  font-family: var(--font-body);
}

/* フィールドラッパー */
.post-form-area .hs-form-field {
  margin-bottom: 20px;
}

/* ラベル */
.post-form-area .hs-form-field > label {
  display: block;
  font-size: 0.88rem;
  font-weight: 700;
  color: var(--color-text);
  margin-bottom: 6px;
}

/* 必須マーク */
.post-form-area .hs-form-required {
  color: #e53e3e;
  margin-left: 3px;
}

/* テキスト入力・セレクト・テキストエリア */
.post-form-area .hs-input,
.post-form-area .hs-form input[type="text"],
.post-form-area .hs-form input[type="email"],
.post-form-area .hs-form input[type="tel"],
.post-form-area .hs-form select,
.post-form-area .hs-form textarea {
  width: 100% !important;   /* インラインスタイル上書き */
  padding: 10px 14px;
  border: 1.5px solid #e2e8f0;
  border-radius: var(--border-radius);
  font-size: 0.93rem;
  font-family: var(--font-body);
  background: #fff;
  color: var(--color-text);
  transition: border-color 0.2s;
  appearance: none;
  -webkit-appearance: none;
}

.post-form-area .hs-form input:focus,
.post-form-area .hs-form select:focus,
.post-form-area .hs-form textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15);
}

/* プレースホルダー */
.post-form-area .hs-form input::placeholder,
.post-form-area .hs-form textarea::placeholder {
  color: #a0aec0;
  font-size: 0.88rem;
}

/* テキストエリア */
.post-form-area .hs-form textarea {
  min-height: 120px;
  resize: vertical;
}

/* チェックボックス・ラジオボタン */
.post-form-area .hs-form .inputs-list {
  list-style: none;
  padding: 0;
}
.post-form-area .hs-form .inputs-list li {
  padding: 4px 0;
}
.post-form-area .hs-form .inputs-list label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 400;
  cursor: pointer;
}

/* 送信ボタン */
.post-form-area .hs-button.primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 14px 28px;
  background: var(--color-primary);
  color: white;
  border: none;
  border-radius: var(--border-radius-btn);
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s, transform 0.1s;
}
.post-form-area .hs-button.primary:hover {
  background: color-mix(in srgb, var(--color-primary) 85%, black);
  transform: translateY(-1px);
}

/* バリデーションエラー */
.post-form-area .hs-error-msgs {
  list-style: none;
  padding: 0;
  margin-top: 4px;
}
.post-form-area .hs-error-msg {
  color: #e53e3e;
  font-size: 0.8rem;
  font-weight: 600;
}
.post-form-area .hs-form input.invalid.error,
.post-form-area .hs-form select.invalid.error {
  border-color: #e53e3e;
}

/* 送信後のサンクスメッセージ */
.post-form-area .submitted-message {
  background: #f0fff4;
  border: 1px solid #9ae6b4;
  border-radius: 8px;
  padding: 24px;
  color: #276749;
  font-weight: 600;
  text-align: center;
}

/* 2カラムレイアウト対応 */
@media (max-width: 640px) {
  .post-form-area .form-columns-2 .hs-form-field {
    width: 100% !important;
    float: none !important;
  }
}
💡 「フォームのデフォルトCSSを無効にする」設定

HubSpot管理画面の「マーケティング → フォーム → 設定」から 「フォームのデフォルトCSSを無効にする」をONにすると、 HubSpotが自動挿入するデフォルトスタイルが読み込まれなくなります。 フルスクラッチでスタイルを組みたい場合はこの設定をONにして、 すべてのスタイルを自分のCSSで定義します。 ただしこの設定はポータル全体に影響するため、既存フォームのデザインが壊れないよう注意してください。


Section 7-5

マルチステップフォームとポップアップフォーム

ポップアップ(モーダル)フォームの実装

CTAボタンをクリックしたときにモーダルでフォームを表示するパターンです。 ランディングページやブログ記事のCTAとして非常によく使われます。

HubL + JS — モーダルフォームの完全実装
{# HTML:モーダルトリガーボタン #}
<button
  class="btn btn--primary js-form-modal-trigger"
  data-form-id="{{ fc.id }}"
  data-form-title="{{ fc.title }}"
  aria-haspopup="dialog">
  {{ fc.cta_label|default("資料をダウンロード") }}
</button>

{# HTML:モーダル本体 #}
<div
  id="form-modal"
  class="form-modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="form-modal-title"
  aria-hidden="true">

  <div class="form-modal__overlay"></div>
  <div class="form-modal__inner">
    <button
      class="form-modal__close"
      aria-label="モーダルを閉じる">✕</button>
    <h2 id="form-modal-title" class="form-modal__title"></h2>
    <div id="form-modal-body"></div>
  </div>
</div>

<script src="//js.hsforms.net/forms/embed/v2.js"></script>
<script>
(function() {
  var modal       = document.getElementById('form-modal');
  var modalTitle  = document.getElementById('form-modal-title');
  var modalBody   = document.getElementById('form-modal-body');
  var closeBtn    = modal.querySelector('.form-modal__close');
  var overlay     = modal.querySelector('.form-modal__overlay');
  var formLoaded  = false;

  // モーダルを開く
  function openModal(formId, formTitle) {
    modalTitle.textContent = formTitle;
    modal.setAttribute('aria-hidden', 'false');
    modal.classList.add('is-open');
    document.body.style.overflow = 'hidden';
    closeBtn.focus();

    // フォームを初回のみ生成(2回目以降は再利用)
    if (!formLoaded) {
      hbspt.forms.create({
        region   : 'na1',
        portalId : '{{ hub_id }}',
        formId   : formId,
        target   : '#form-modal-body',
        onFormSubmitted: function() {
          if (window.dataLayer) {
            window.dataLayer.push({ event: 'hs_form_submit' });
          }
        }
      });
      formLoaded = true;
    }
  }

  // モーダルを閉じる
  function closeModal() {
    modal.setAttribute('aria-hidden', 'true');
    modal.classList.remove('is-open');
    document.body.style.overflow = '';
  }

  // トリガーボタンのイベント
  document.querySelectorAll('.js-form-modal-trigger').forEach(function(btn) {
    btn.addEventListener('click', function() {
      openModal(
        this.dataset.formId,
        this.dataset.formTitle
      );
    });
  });

  // 閉じるボタン・オーバーレイクリック
  closeBtn.addEventListener('click', closeModal);
  overlay.addEventListener('click', closeModal);

  // ESCキーで閉じる
  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && modal.classList.contains('is-open')) {
      closeModal();
    }
  });
})();
</script>

Section 7-6

サンクスページとリダイレクト設計

コンバージョンフロー全体像

📄
LP・記事
コンテンツ
📝
フォーム送信
コンバージョン
サンクスページ
/thanks/xxx
📧
メール配信
ワークフロー

コンテンツ種別ごとのサンクスページ設計

コンテンツ種別サンクスページURLページの内容
お問い合わせ/thanks/contact「担当者よりご連絡します」+ブログへの誘導
セミナー申込/thanks/seminar「申込完了。確認メールをご確認ください」+Zoom URL(ワークフローで自動送付)
資料DL/thanks/whitepaper「ダウンロードはこちら」ボタン+関連記事
事例資料請求/thanks/case「メールをお送りしました」+他の事例へのリンク

サンクスページの実装パターン

templates/thanks-page.html — サンクスページ実装

{% extends "./layouts/base.html" %}

{% block main_content %}
<div class="thanks-page">

  {# ====== サンクスメッセージ(モジュールで管理) ====== #}
  {% module "thanks_message"
    path="../modules/thanks-hero.module"
    label="サンクスメッセージ"
  %}

  {# ====== 資料DLサンクスページの場合のみ:ダウンロードボタン表示 ====== #}
  {# URLのパスで種別を判定する #}
  {% if request.path starts_with "/thanks/whitepaper" %}
    {% module "download_button"
      path="../modules/download-cta.module"
      label="ダウンロードCTA"
    %}
  {% endif %}

  {# ====== 次のアクション誘導 ====== #}
  <section class="thanks-next">
    <h2>あわせてご覧ください</h2>
    {% set recommend_posts = blog_recent_posts("default", 3) %}
    {% if recommend_posts %}
      <ul class="thanks-next__list">
        {% for post in recommend_posts %}
          <li>
            <a href="{{ post.absolute_url }}">
              {% if post.featured_image %}
                <img src="{{ post.featured_image }}"
                     alt="{{ post.featured_image_alt_text|default(post.name) }}"
                     loading="lazy">
              {% endif %}
              <p>{{ post.name }}</p>
            </a>
          </li>
        {% endfor %}
      </ul>
    {% endif %}
  </section>

</div>
{% endblock %}
ℹ️ サンクスページのSEO設定

サンクスページは検索エンジンにインデックスされると フォームを経由せずに直接アクセスされ、コンバージョン計測が汚染されます。 必ず noindex, nofollow を設定してください。
HubSpot管理画面のページ設定から「検索エンジンへのインデックスを許可しない」をONにするか、 テンプレートのheadに <meta name="robots" content="noindex, nofollow"> を追加します。


Section 7-7 ★ 2025年更新

CTAの設計と実装パターン

⚠️ 旧CTAエディター(クラシックCTA)は2025年11月30日に廃止されました

HubSpotの旧CTAエディターは2025年11月30日をもって廃止されました。 既存の旧CTAはそのまま動作・表示されますが、旧エディターでの新規CTA作成は不可になっています。 新規CTAは「マーケティング → コンテンツ → CTA」から新CTAエディターを使用してください。

また、HubLテンプレートで旧CTAを埋め込んでいた {% cta %} タグや hbspt.cta.load() の JavaScript による埋め込みは引き続き動作しますが、 新規実装ではカスタムモジュール + リンクフィールドによるCTAボタン実装(下記のコード例)を推奨します。

💡 新CTAエディターの主な変化点

新CTAエディターは、従来の「CTA オブジェクト」の概念から離れ、スマートコンテンツと統合されたコンテンツブロック型のCTAとして再設計されています。 テンプレートへの直接埋め込みよりも、ページエディタ上でマーケターが配置・管理する運用が前提のUIになっています。 開発者としては、CTAボタン・バナーモジュールをコードで実装し、マーケターが内容を差し替えられる設計にするアプローチが現在のベストプラクティスです。

CTAの種類と使い分け

インラインCTA

テキスト・画像ボタン

記事本文の途中・末尾に自然な流れで配置。文脈に合わせたオファーを提示する。

バナーCTA

セクションバナー

ページの区切りに配置する横幅いっぱいのCTAブロック。視認性が高い。

スティッキーCTA

固定表示ボタン

スクロールしても画面に固定表示されるフローティングボタン。常に行動を促す。

ポップアップCTA

モーダル・スライドイン

一定スクロール後や離脱意図検知時にポップアップで表示。高い注目度。

CTAバナーモジュールの実装

modules/cta-banner.module / module.html
{#
  fields.json に以下を定義:
  - bg_color (color)
  - eyecatch (image)
  - label (text) : 小見出し
  - title (text) : メイン見出し
  - desc (richtext) : 説明文
  - cta_label (text) : ボタンテキスト
  - cta_link (link) : ボタンリンク
  - cta_style (choice) : primary / secondary / ghost
  - open_form_modal (boolean) : クリックでモーダルフォームを開くか
  - form_id (text) : モーダル用フォームID(open_form_modal=trueの場合)
#}

<section
  class="cta-banner"
  style="background-color: {{ module.bg_color.color|default("#0052CC") }};"
  {% if module.eyecatch.src %}
    style="background-image: url('{{ module.eyecatch.src }}'); background-size: cover;"
  {% endif %}>

  <div class="cta-banner__inner">
    {% if module.label %}
      <p class="cta-banner__label">{{ module.label }}</p>
    {% endif %}
    <h2 class="cta-banner__title">{{ module.title }}</h2>
    {% if module.desc %}
      <div class="cta-banner__desc">{{ module.desc }}</div>
    {% endif %}

    {% if module.open_form_modal and module.form_id %}
      {# モーダルで開く場合 #}
      <button
        class="btn btn--{{ module.cta_style|default("primary") }} js-form-modal-trigger"
        data-form-id="{{ module.form_id }}"
        data-form-title="{{ module.title }}">
        {{ module.cta_label }}
      </button>
    {% elif module.cta_link.url %}
      {# 通常リンク #}
      <a
        href="{{ module.cta_link.url }}"
        class="btn btn--{{ module.cta_style|default("primary") }}"
        {% if module.cta_link.open_in_new_tab %}
          target="_blank" rel="noopener noreferrer"
        {% endif %}>
        {{ module.cta_label }}
      </a>
    {% endif %}
  </div>
</section>

スティッキーCTAの実装

HubL + JS — スクロールで表示されるスティッキーCTA
{# base.html の body 末尾に追加 #}
<div id="sticky-cta" class="sticky-cta" aria-hidden="true">
  {% module "sticky_cta_content"
    path="../modules/sticky-cta.module"
    label="スティッキーCTA"
  %}
</div>

<script>
(function() {
  var stickyCta = document.getElementById('sticky-cta');
  if (!stickyCta) return;

  var SHOW_THRESHOLD  = 400;   // 何px スクロールしたら表示するか
  var HIDE_NEAR_FORM  = true;  // フォームに近づいたら非表示にするか
  var formSection = document.querySelector('.post-form-area');

  window.addEventListener('scroll', function() {
    var scrollY = window.scrollY || window.pageYOffset;

    if (scrollY > SHOW_THRESHOLD) {
      // フォームセクションが近くなったら非表示
      if (HIDE_NEAR_FORM && formSection) {
        var formTop = formSection.getBoundingClientRect().top;
        if (formTop < window.innerHeight * 1.2) {
          stickyCta.classList.remove('is-visible');
          stickyCta.setAttribute('aria-hidden', 'true');
          return;
        }
      }
      stickyCta.classList.add('is-visible');
      stickyCta.setAttribute('aria-hidden', 'false');
    } else {
      stickyCta.classList.remove('is-visible');
      stickyCta.setAttribute('aria-hidden', 'true');
    }
  }, { passive: true });
})();
</script>

Section 7-8

コンバージョン計測(GA4 / GTM連携)

フォーム送信をGA4・GTMで計測することで、 どのページ・コンテンツ・流入経路がコンバージョンに貢献しているかを把握できます。 HubSpotフォームとGA4/GTMの連携方法を整理します。

GTMの設定とHubSpotフォームのdataLayerイベント

JS — GTM dataLayer を使ったフォーム送信計測
// hbspt.forms.create の onFormSubmitted コールバックで送信
hbspt.forms.create({
  region  : 'na1',
  portalId: '{{ hub_id }}',
  formId  : '{{ fc.id }}',
  target  : '#hs-post-form',

  onFormSubmitted: function($form, data) {

    // ===== GTM dataLayer にプッシュ =====
    if (window.dataLayer) {
      window.dataLayer.push({
        'event'        : 'hs_form_submit',
        'formType'     : '{{ ns.content_type }}',
        'formTitle'    : '{{ fc.title|escape }}',
        'pagePath'     : window.location.pathname,
        'pageTitle'    : document.title
      });
    }

    // ===== GA4 に直接送信(GTMを使わない場合)=====
    if (window.gtag) {
      gtag('event', 'generate_lead', {
        'event_category' : 'form',
        'event_label'    : '{{ fc.title|escape }}',
        'content_type'   : '{{ ns.content_type }}'
      });
    }
  }
});

HubSpotのネイティブ計測とGA4の連携

計測手段設定場所できること
HubSpot Analytics HubSpot管理画面 フォーム送信数・コンタクト獲得数・ページビューなどを管理画面で確認
GA4(直接連携) HubSpot設定 → トラッキングコード → GA4連携 HubSpotがGA4のgtag.jsを自動挿入。基本的なPV計測が自動で行われる
GTM経由 HubSpot設定 → トラッキングコード → GTMコンテナID GTMコンテナをHubSpotページに挿入。GTMでGA4設定・カスタムイベントを管理
onFormSubmitted コールバック JS実装(本節) フォーム種別・コンテンツ種別などカスタムパラメータ付きでGA4/GTMに送信

GTMトリガー設定(GTMコンソール側の設定)

GTM設定ガイド — hs_form_submit イベントのGA4コンバージョン設定
// GTM管理画面での設定内容

【トリガー】
  種類         :カスタムイベント
  イベント名   :hs_form_submit
  このトリガーの発生 :すべてのカスタムイベント

【変数】(データレイヤー変数として追加)
  formType    ← dataLayer の formType
  formTitle   ← dataLayer の formTitle
  pagePath    ← dataLayer の pagePath

【タグ:GA4 イベント】
  タグの種類   :Google アナリティクス: GA4 イベント
  測定 ID      :G-XXXXXXXXXX
  イベント名   :generate_lead
  イベントパラメータ:
    form_type   → {{formType}}
    form_title  → {{formTitle}}
    page_path   → {{pagePath}}

【GA4 でコンバージョンとして設定】
  GA4 管理画面 → イベント → generate_lead を「コンバージョンとしてマーク」
✅ HubSpotとGA4の二重計測に注意

HubSpotの「GA4連携」設定とGTMを両方有効にすると二重計測になります。 GTM経由でGA4を管理する場合は、HubSpotのGA4直接連携設定をOFFにしてください。 どちらか一方に統一することが計測精度を保つ基本原則です。

サンクスページでのコンバージョン計測

HubL — サンクスページ専用のGA4コンバージョン送信
{# サンクスページのテンプレートに追加 #}
{% block extra_js %}
<script>
  // サンクスページ到達 = コンバージョン確定として計測
  // ページのURLパスで種別を判定
  (function() {
    var path        = window.location.pathname;
    var formTypeMap = {
      '/thanks/seminar'    : 'seminar',
      '/thanks/whitepaper' : 'whitepaper',
      '/thanks/case'       : 'case',
      '/thanks/contact'    : 'contact'
    };

    var formType = 'unknown';
    Object.keys(formTypeMap).forEach(function(key) {
      if (path.startsWith(key)) formType = formTypeMap[key];
    });

    // GTM dataLayer
    if (window.dataLayer) {
      window.dataLayer.push({
        'event'    : 'conversion_pageview',
        'formType' : formType
      });
    }

    // GA4 直接
    if (window.gtag) {
      gtag('event', 'generate_lead', {
        method      : 'thanks_page',
        content_type: formType
      });
    }
  })();
</script>
{% endblock %}

Section 7-9

第7章まとめ

📌 この章で押さえるべきポイント

2つの実装方式の使い分け

HubL form タグは固定配置に最適。hbspt.forms.create はフォームIDの動的切り替え・onFormSubmittedコールバックが必要な場合に使う。

コンテンツ種別 × フォーム連動

タグ判定 → 辞書でフォームID・タイトル・リダイレクト先を定義 → HubL変数をJSに橋渡しする3ステップパターンが実務の核心。

フォームCSS上書き

親クラスでスコープを限定してセレクタ優先度を上げる。インラインスタイルは !important で上書き。デフォルトCSS無効設定で完全制御も可能。

サンクスページ設計

種別ごとに別URLのサンクスページを用意する。必ずnoindex設定をする。資料DLは直接ダウンロードリンクを表示。

hidden フィールドの活用

form_field_values_json でページのコンテキスト情報をCRMに自動セット。営業への引き渡し品質が上がる。フォーム側にhiddenフィールドの追加が必要。

コンバージョン計測

GTM × onFormSubmitted でフォーム種別・コンテンツ種別付きのイベントを送信。GA4とGTMの二重計測に注意。サンクスページ到達での計測も併用する。

次章:第8章 パフォーマンス・SEO・アクセシビリティ

Core Web Vitals 最適化・画像の遅延読み込み・HubSpot CDNの活用・構造化データ・アクセシビリティ実装まで解説します。

第8章へ →