自作HTMLフォームをGoogleフォームとGASで連携する完全手順 2026年版 確認画面 自動返信 スパム対策まで実務で迷わない

Web技術

お問い合わせフォームを作りたい。でもサーバー側の実装は出来れば避けたい。回答はスプレッドシートで管理したい。ついでに自動返信も欲しい。しかも出来れば確認画面付きでユーザー体験も落としたくない。

この条件、制作会社やフリーランスの現場だとかなりの頻度で出てきます。そこで強いのが「自作HTMLフォーム + Googleフォーム + Google Apps Script(GAS)」の組み合わせです。

ただし、この構成は記事を読む人が多い一方で、途中離脱も起きやすいです。理由はシンプルで、つまずきポイントが多いからです。

  • entry ID が分からず止まる
  • GoogleフォームにPOSTした後の遷移が不安定
  • 確認画面で値が保持されない
  • 自動返信トリガーが動かない
  • スパム対策やセキュリティ面が不安で手が止まる
  1. まず結論 この構成が刺さる場面と 刺さらない場面
    1. この構成が向いているケース
    2. この構成が向いていないケース
  2. 最短ルート これだけ読めば今日中に動く
  3. 全体構成図 実務で壊れにくい形にする
  4. なぜ止める必要があるのか よくあるNG設計と離脱の原因
    1. NG1 確認画面の値を URLパラメータで渡す
    2. NG2 GoogleフォームへいきなりPOSTして 完了画面が不安定
    3. NG3 entry ID が曖昧でコピペして動かない
    4. NG4 スパム対策や注意点が後半にしかない
  5. Step1 Googleフォームを作る 項目設計のコツ
  6. Step2 entry ID を取得する ここで詰まらない方法
    1. entry ID の見つけ方(おすすめ手順)
  7. Step3 ファイル構成 迷わない最小セット
  8. Step4 入力画面 index.html を作る
  9. Step5 確認画面 confirm.html を作る
  10. Step6 完了画面 complete.html を作る
  11. Step7 JavaScript form.js 入力保持と確認表示と送信をまとめる
  12. Step8 最低限のCSS style.css 読みやすさは離脱率に効く
  13. Step9 GASで自動返信を付ける ここから実務感が出る
    1. GASコード(onFormSubmit)
    2. トリガー設定手順(ここで止まりやすい)
  14. メリット この構成が選ばれる理由
  15. デメリット 注意点 ここを知らないと後で詰む
    1. 1 セキュリティは万能ではない
    2. 2 entry ID と質問タイトル変更に弱い
    3. 3 スパム対策を入れないと地獄を見る
  16. スパム対策 実務で最低限やるセット
    1. 1 honeypot(ダミー入力欄)
    2. 2 送信ボタンの二重送信防止
    3. 3 運用側のフィルタ
  17. よくあるトラブル集 ここだけ読めば復旧できる
    1. 送信したのにスプレッドシートに入らない
    2. confirm.html に来たら空になる
    3. 自動返信が届かない
  18. ここまで出来たら応用編 実務で喜ばれる追加機能
    1. 確認画面で改行やURLをきれいに表示する
    2. 送信内容を管理者にも通知する
    3. 確認画面に編集導線を入れる
  19. まとめ 自作HTML + Googleフォーム + GAS は 小規模案件の最適解になりやすい
  20. 読者向けの有益情報 さらに安全に運用するためのチェックリスト

まず結論 この構成が刺さる場面と 刺さらない場面

この構成が向いているケース

  • LPや小規模サイトで お問い合わせや資料請求を素早く用意したい
  • 回答管理をスプレッドシートで完結させたい
  • サーバー側の実装やDBを持たずに運用したい
  • 確認画面や自動返信など 最低限のUXは欲しい

この構成が向いていないケース

  • 決済や会員登録など 高いセキュリティ要件がある
  • 送信データの厳密な検証や 不正検知 ログ保管が必要
  • 大量トラフィックでスパムが多く SLAが要求される
  • フォームの仕様が頻繁に変わり 型やバリデーションが複雑

要するに「中小規模で確実に動くフォームを早く作る」には強い。一方で「プロダクトの核になるフォーム」には専用のバックエンドやフォームサービスを検討した方が安全です。

最短ルート これだけ読めば今日中に動く

最短で動かすために、先に全体の流れを固定します。ここが曖昧だと途中で迷子になります。

  1. Googleフォームを作る(項目は 名前 メール 内容)
  2. 回答先スプレッドシートを作る(自動で作成される)
  3. entry ID を取得して 自作フォームの hidden に割り当てる
  4. 入力ページ(index.html)で入力した値を sessionStorage に保存
  5. 確認ページ(confirm.html)で表示し hidden に詰め直して Googleフォームへ送信
  6. 完了ページ(complete.html)へ遷移する(送信後の見せ方を安定させる)
  7. GASで onFormSubmit トリガーを作り 自動返信を送る

この順番で進めれば、手戻りが激減します。以下、必要なものを順に作っていきます。

全体構成図 実務で壊れにくい形にする

index.html(入力)
  ↓(sessionStorageに保存して遷移)
confirm.html(確認)
  ↓(GoogleフォームへPOST)
Googleフォーム(formResponse)
  ↓(回答がスプレッドシートに保存)
スプレッドシート
  ↓(フォーム送信トリガー)
GAS(onFormSubmit)
  ↓
自動返信メール
  ↓
complete.html(完了表示)

ポイントは「確認画面のデータ受け渡しを sessionStorage で行う」ことです。URLパラメータに載せないので、見た目も安全性も上がり、戻る操作も扱いやすくなります。

なぜ止める必要があるのか よくあるNG設計と離脱の原因

NG1 確認画面の値を URLパラメータで渡す

手軽に見えますが、文字数制限や文字化け、履歴に残る問題が出やすいです。長文の問い合わせで破綻し、そこで離脱が起きます。

NG2 GoogleフォームへいきなりPOSTして 完了画面が不安定

Googleフォームの送信後挙動は、埋め込み方やブラウザ仕様で見え方が揺れます。完了表示が出ない、戻ると二重送信、などが起きやすい。ユーザー不安で離脱します。

NG3 entry ID が曖昧でコピペして動かない

ここで詰まる人が多いです。動かない時の原因が分かりにくく、記事を閉じる。PVが多いのに離脱が高い典型です。

NG4 スパム対策や注意点が後半にしかない

真面目な人ほど、途中で「これ運用して大丈夫なのか」と不安になります。安心材料が早めに出ていないと離脱します。

このリライトでは、上の離脱原因を先に潰します。ここから手順です。

Step1 Googleフォームを作る 項目設計のコツ

まずGoogleフォームで以下の3項目を作ります。

  • お名前(短い回答)
  • メールアドレス(短い回答)
  • お問い合わせ内容(段落)

実務のコツは「フォームの必須判定はGoogleフォーム側でも付ける」ことです。自作HTML側の required は便利ですが、クライアント側だけだと回避される可能性があります。最終防衛ラインとしてGoogleフォーム側にも必須を付けます。

Step2 entry ID を取得する ここで詰まらない方法

entry ID は GoogleフォームにPOSTする時のキーです。ここが一致しないと、送っても記録されません。

entry ID の見つけ方(おすすめ手順)

  1. Googleフォームのプレビューを開く
  2. ブラウザの開発者ツールを開く
  3. 要素を調べるで input や textarea を選ぶ
  4. name 属性に entry.xxxxxx があるのでメモする

例えば name=”entry.123456789″ のような値です。これを自作フォームの hidden に割り当てます。

ここが面倒に感じる場合は、最初に送信テスト用の簡単なHTMLを作って、とにかく1回スプレッドシートに入る所まで行くのがコツです。動く感覚を掴んだ後に確認画面や自動返信を足すと、途中離脱が減ります。人間もフォームも、成功体験が大事です。

Step3 ファイル構成 迷わない最小セット

/form-sample/
  index.html
  confirm.html
  complete.html
  assets/
    form.js
    style.css

最小でこの5つにします。CSSは無くても動きますが、確認画面が読みづらいとユーザーが不安になります。最低限の見た目は早めに整えた方が離脱率が下がります。

Step4 入力画面 index.html を作る

入力ページは、送信ボタンを押したら confirm.html に遷移するだけにします。ここでGoogleフォームへ送らないのがポイントです。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>お問い合わせフォーム</title>
  <link rel="stylesheet" href="assets/style.css">
</head>
<body>

<main class="container">
  <h1>お問い合わせフォーム</h1>

  <form id="inputForm">

    <div class="field">
      <label for="name">お名前</label>
      <input id="name" name="name" type="text" autocomplete="name" required>
    </div>

    <div class="field">
      <label for="email">メールアドレス</label>
      <input id="email" name="email" type="email" autocomplete="email" required>
    </div>

    <div class="field">
      <label for="message">お問い合わせ内容</label>
      <textarea id="message" name="message" rows="6" required></textarea>
    </div>

    <button class="btn" type="submit">内容を確認する</button>

  </form>

  <p class="note">送信前に確認画面が表示されます。</p>
</main>

<script src="assets/form.js"></script>
</body>
</html>

この段階で「内容を確認する」を押したら confirm.html に移動し、入力値を引き継げれば勝ちです。

Step5 確認画面 confirm.html を作る

確認画面の目的は2つです。

  • 入力ミスを減らす(クレームや再対応が減る)
  • 送信前の不安を減らす(離脱が減る)

構造は dl dt dd が読みやすいです。送信用のformは hidden に entry ID を入れます。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>確認画面</title>
  <link rel="stylesheet" href="assets/style.css">
</head>
<body>

<main class="container">
  <h1>ご入力内容の確認</h1>

  <dl class="confirm" id="confirmList"></dl>

  <div class="actions">
    <button class="btn is-sub" type="button" id="backBtn">戻る</button>
    <button class="btn" type="button" id="submitBtn">この内容で送信する</button>
  </div>

  <form id="googleForm"
    action="https://docs.google.com/forms/d/e/YOUR_FORM_ID/formResponse"
    method="post"
    target="hidden_iframe">

    <input type="hidden" name="entry.111111111" id="entryName">
    <input type="hidden" name="entry.222222222" id="entryEmail">
    <input type="hidden" name="entry.333333333" id="entryMessage">

  </form>

  <iframe name="hidden_iframe" id="hidden_iframe" style="display:none"></iframe>

  <p class="note">送信後は完了画面へ移動します。</p>
</main>

<script src="assets/form.js"></script>
</body>
</html>

重要なのは2点です。

  • YOUR_FORM_ID を自分のフォームIDに置き換える
  • entry.111… を実際の entry ID に置き換える

target を hidden_iframe にしているのは、Googleフォーム送信後の画面遷移の不安定さを避けるためです。送信自体は裏で行い、こちらは自前で complete.html に遷移させます。これで完了表示が安定し、ユーザーが迷いにくくなります。

Step6 完了画面 complete.html を作る

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>送信完了</title>
  <link rel="stylesheet" href="assets/style.css">
</head>
<body>

<main class="container">
  <h1>送信が完了しました</h1>
  <p>お問い合わせありがとうございます。確認のため、自動返信メールをお送りします。</p>

  <div class="box">
    <p>数分たってもメールが届かない場合は、迷惑メールフォルダもご確認ください。</p>
  </div>

  <a class="btn is-sub" href="index.html">入力画面に戻る</a>
</main>

</body>
</html>

完了画面の文言は短くてOKです。ここで長文説明をすると、ユーザーは読まずに閉じます。離脱を減らしたいなら「安心できる一言 + 次にやる事」だけで十分です。

Step7 JavaScript form.js 入力保持と確認表示と送信をまとめる

ここが一番の肝です。難しく見えますが、やる事は3つだけです。

  • index.html で入力値を sessionStorage に保存して confirm.html へ
  • confirm.html で sessionStorage から値を取り出して表示し hidden に詰める
  • 送信ボタンで Googleフォームへ送信し complete.html へ遷移
(() => {
  const KEY = "contactFormData";

  const isIndex = /index\.html$/.test(location.pathname) || location.pathname.endsWith("/");
  const isConfirm = /confirm\.html$/.test(location.pathname);

  const saveData = (data) => {
    sessionStorage.setItem(KEY, JSON.stringify(data));
  };

  const loadData = () => {
    const raw = sessionStorage.getItem(KEY);
    if (!raw) return null;
    try { return JSON.parse(raw); } catch (e) { return null; }
  };

  const escapeText = (v) => {
    return String(v ?? "")
      .replaceAll("&", "&amp;")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;")
      .replaceAll("'", "&#39;");
  };

  if (document.getElementById("inputForm")) {
    const form = document.getElementById("inputForm");
    form.addEventListener("submit", (e) => {
      e.preventDefault();

      const data = {
        name: form.name.value.trim(),
        email: form.email.value.trim(),
        message: form.message.value.trim()
      };

      if (!data.name || !data.email || !data.message) {
        alert("未入力の項目があります。");
        return;
      }

      saveData(data);
      location.href = "confirm.html";
    });
  }

  if (isConfirm && document.getElementById("confirmList")) {
    const data = loadData();
    if (!data) {
      location.href = "index.html";
      return;
    }

    const list = document.getElementById("confirmList");
    const rows = [
      { label: "お名前", value: data.name },
      { label: "メールアドレス", value: data.email },
      { label: "お問い合わせ内容", value: data.message }
    ];

    list.innerHTML = rows.map(r => (
      `<dt>${escapeText(r.label)}</dt><dd>${escapeText(r.value)}</dd>`
    )).join("");

    const nameEl = document.getElementById("entryName");
    const emailEl = document.getElementById("entryEmail");
    const msgEl = document.getElementById("entryMessage");

    if (nameEl) nameEl.value = data.name;
    if (emailEl) emailEl.value = data.email;
    if (msgEl) msgEl.value = data.message;

    const backBtn = document.getElementById("backBtn");
    if (backBtn) {
      backBtn.addEventListener("click", () => {
        location.href = "index.html";
      });
    }

    const submitBtn = document.getElementById("submitBtn");
    const gform = document.getElementById("googleForm");

    if (submitBtn && gform) {
      submitBtn.addEventListener("click", () => {
        submitBtn.disabled = true;
        submitBtn.textContent = "送信中...";

        try {
          gform.submit();
        } catch (e) {
          submitBtn.disabled = false;
          submitBtn.textContent = "この内容で送信する";
          alert("送信に失敗しました。もう一度お試しください。");
          return;
        }

        sessionStorage.removeItem(KEY);

        setTimeout(() => {
          location.href = "complete.html";
        }, 600);
      });
    }
  }
})();

この form.js は、確認画面に来た時にデータが無ければ index に戻すので、直接URLを叩かれても迷子になりにくいです。これも離脱を減らす地味な工夫です。

Step8 最低限のCSS style.css 読みやすさは離脱率に効く

実務だとデザインに合わせますが、検証段階で最低限読みやすくするだけでも完成率が上がります。

.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 16px;
  font-family: system-ui, -apple-system, "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif;
  line-height: 1.7;
}

.field { margin-bottom: 16px; }
label { display: block; font-weight: 700; margin-bottom: 6px; }

input, textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #ccc;
  border-radius: 8px;
  font-size: 16px;
}

.btn {
  display: inline-block;
  width: 100%;
  padding: 12px 14px;
  border: 0;
  border-radius: 10px;
  background: #111;
  color: #fff;
  font-weight: 700;
  cursor: pointer;
  text-align: center;
}

.btn.is-sub {
  background: #f2f2f2;
  color: #111;
}

.actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 16px;
}

.confirm dt {
  font-weight: 700;
  margin-top: 12px;
}
.confirm dd {
  margin: 6px 0 0;
  padding: 10px 12px;
  border: 1px solid #e1e1e1;
  border-radius: 8px;
  background: #fafafa;
  white-space: pre-wrap;
}

.note { margin-top: 12px; color: #555; font-size: 14px; }

.box {
  border: 1px solid #e1e1e1;
  background: #fafafa;
  padding: 12px;
  border-radius: 10px;
  margin: 16px 0;
}

確認画面の dd に white-space: pre-wrap; を入れているのは、お問い合わせ内容の改行を保つためです。ここが潰れると「ちゃんと入力できたのかな」と不安になり、離脱に繋がります。

Step9 GASで自動返信を付ける ここから実務感が出る

Googleフォーム単体でも回答は溜まりますが、自動返信があるとユーザーの安心感が段違いです。返信が無いと「送れてるのか不安」で再送され、運用側が疲れます。

GASコード(onFormSubmit)

スプレッドシートの「拡張機能 -> Apps Script」で以下を作ります。項目名(namedValuesのキー)は、Googleフォームの質問タイトルと一致します。タイトルを変えたらここも変わるので、運用で変える予定があるなら固定の命名にしておくのがコツです。

function onFormSubmit(e) {
  const v = e.namedValues;

  const name = (v["お名前"] && v["お名前"][0]) ? v["お名前"][0] : "";
  const email = (v["メールアドレス"] && v["メールアドレス"][0]) ? v["メールアドレス"][0] : "";
  const message = (v["お問い合わせ内容"] && v["お問い合わせ内容"][0]) ? v["お問い合わせ内容"][0] : "";

  if (!email) return;

  const subject = "お問い合わせありがとうございます(自動返信)";
  const body =
    name + " 様\n\n" +
    "お問い合わせありがとうございます。以下の内容で受け付けました。\n\n" +
    "【お問い合わせ内容】\n" +
    message + "\n\n" +
    "担当者より順次ご連絡いたします。\n\n" +
    "--------------------------------\n" +
    "会社名や屋号\n" +
    "URL\n" +
    "メール\n" +
    "--------------------------------\n";

  MailApp.sendEmail({
    to: email,
    subject: subject,
    body: body,
    replyTo: "support@example.com"
  });
}

トリガー設定手順(ここで止まりやすい)

  1. Apps Script 画面で トリガー を開く
  2. 関数を onFormSubmit にする
  3. イベントのソースを フォーム送信元のスプレッドシート にする
  4. イベントタイプを フォーム送信時 にする
  5. 保存して 権限を承認する

権限承認は初回に必ず出ます。ここで戻ると永遠に動きません。地味ですが最大のつまずきポイントです。

メリット この構成が選ばれる理由

  • バックエンド不要で運用開始が早い
  • 回答が自動でスプレッドシートに溜まるので管理が楽
  • 確認画面と完了画面でUXを底上げ出来る
  • GASで自動返信や通知など拡張が出来る
  • 小規模案件ならコストを抑えて実装出来る

デメリット 注意点 ここを知らないと後で詰む

1 セキュリティは万能ではない

この構成は便利ですが、アプリケーションのセキュリティ要件を全部満たすものではありません。高い要件がある場合は専用のバックエンドやフォームサービスを検討してください。

2 entry ID と質問タイトル変更に弱い

Googleフォームの項目を変えると entry ID が変わる可能性があります。フォームを運用で改修するなら、変更手順(entry ID再取得とGASのキー確認)をチームで共有するのが安全です。

3 スパム対策を入れないと地獄を見る

公開フォームはスパムが来ます。少ないうちは笑えますが、増えると笑えません。最低限の対策を最初から入れましょう。

スパム対策 実務で最低限やるセット

1 honeypot(ダミー入力欄)

ユーザーには見えない入力欄を仕込み、そこに値が入っていたら送信しない。古典ですが効きます。

<!-- index.html の form 内に追加 -->
<div style="position:absolute;left:-9999px;top:-9999px">
  <label>company<input type="text" name="company" autocomplete="off"></label>
</div>

form.js 側で company に値があれば confirm に進めないようにします。

if (form.company && form.company.value.trim()) {
  return;
}

2 送信ボタンの二重送信防止

confirm の送信ボタンを disabled にして文言を変える。これだけで二重送信が減ります。上の form.js で実装済みです。

3 運用側のフィルタ

スプレッドシート側で怪しいメールドメインや特定文言をフィルタして、担当者の視界から消すだけでも運用負荷が下がります。

よくあるトラブル集 ここだけ読めば復旧できる

送信したのにスプレッドシートに入らない

  • entry ID が違う可能性が高い
  • formResponse のURLが間違っている可能性がある
  • name属性が entry.xxxxxx になっているか確認

confirm.html に来たら空になる

  • sessionStorage が保存されていない
  • index.html の submit を preventDefault しているか確認
  • ブラウザでプライベートモードだと制限が出る場合がある

自動返信が届かない

  • トリガー設定が出来ていない
  • 権限承認が完了していない
  • GASの namedValues のキー(質問タイトル)が一致していない

ここまで出来たら応用編 実務で喜ばれる追加機能

確認画面で改行やURLをきれいに表示する

white-space: pre-wrap; は必須級です。URLが長い場合は overflow-wrap: anywhere; を追加するとレイアウト崩れを防げます。

.confirm dd {
  overflow-wrap: anywhere;
}

送信内容を管理者にも通知する

自動返信だけでなく、運用側への通知メールもGASで可能です。担当アドレスを固定で入れ、同じタイミングで送ります。

MailApp.sendEmail("admin@example.com", "フォーム通知", "新規問い合わせがありました。");

確認画面に編集導線を入れる

戻るボタンで index に戻した後、入力が消えるとストレスです。必要なら index 側で sessionStorage を読み込み、初期値として復元するとUXが上がります。

まとめ 自作HTML + Googleフォーム + GAS は 小規模案件の最適解になりやすい

この構成は、正しく組めば安定します。ポイントは「離脱しやすい所を最初から潰す」ことです。

  • entry ID を確実に取得して一致させる
  • 確認画面は sessionStorage で保持して迷子を減らす
  • 送信後は hidden iframe + 自前の完了画面で安定させる
  • GASのトリガーと権限承認を確実に行う
  • 最低限のスパム対策を最初から入れる

フォームは見た目より運用が本番です。最初に少し丁寧に作っておくと、後で問い合わせ対応も修正工数も減ります。つまり未来の自分が助かります。未来の自分は、だいたい今より忙しいので、助けておいた方が良いです。

読者向けの有益情報 さらに安全に運用するためのチェックリスト

  • Googleフォーム側でも必須設定を入れたか
  • entry ID を再確認したか(特にコピペ後)
  • 送信後の完了表示が安定しているか(スマホでも確認)
  • 二重送信防止が入っているか
  • スパム対策(honeypot など)を入れたか
  • GASトリガーがフォーム送信時になっているか
  • 権限承認が完了しているか
  • 問い合わせ文の改行が確認画面で潰れていないか

(Visited 135 times, 1 visits today)
タイトルとURLをコピーしました