お問い合わせフォームを作りたい。でもサーバー側の実装は出来れば避けたい。回答はスプレッドシートで管理したい。ついでに自動返信も欲しい。しかも出来れば確認画面付きでユーザー体験も落としたくない。
この条件、制作会社やフリーランスの現場だとかなりの頻度で出てきます。そこで強いのが「自作HTMLフォーム + Googleフォーム + Google Apps Script(GAS)」の組み合わせです。
ただし、この構成は記事を読む人が多い一方で、途中離脱も起きやすいです。理由はシンプルで、つまずきポイントが多いからです。
- entry ID が分からず止まる
- GoogleフォームにPOSTした後の遷移が不安定
- 確認画面で値が保持されない
- 自動返信トリガーが動かない
- スパム対策やセキュリティ面が不安で手が止まる
- まず結論 この構成が刺さる場面と 刺さらない場面
- 最短ルート これだけ読めば今日中に動く
- 全体構成図 実務で壊れにくい形にする
- なぜ止める必要があるのか よくあるNG設計と離脱の原因
- Step1 Googleフォームを作る 項目設計のコツ
- Step2 entry ID を取得する ここで詰まらない方法
- Step3 ファイル構成 迷わない最小セット
- Step4 入力画面 index.html を作る
- Step5 確認画面 confirm.html を作る
- Step6 完了画面 complete.html を作る
- Step7 JavaScript form.js 入力保持と確認表示と送信をまとめる
- Step8 最低限のCSS style.css 読みやすさは離脱率に効く
- Step9 GASで自動返信を付ける ここから実務感が出る
- メリット この構成が選ばれる理由
- デメリット 注意点 ここを知らないと後で詰む
- スパム対策 実務で最低限やるセット
- よくあるトラブル集 ここだけ読めば復旧できる
- ここまで出来たら応用編 実務で喜ばれる追加機能
- まとめ 自作HTML + Googleフォーム + GAS は 小規模案件の最適解になりやすい
- 読者向けの有益情報 さらに安全に運用するためのチェックリスト
まず結論 この構成が刺さる場面と 刺さらない場面
この構成が向いているケース
- LPや小規模サイトで お問い合わせや資料請求を素早く用意したい
- 回答管理をスプレッドシートで完結させたい
- サーバー側の実装やDBを持たずに運用したい
- 確認画面や自動返信など 最低限のUXは欲しい
この構成が向いていないケース
- 決済や会員登録など 高いセキュリティ要件がある
- 送信データの厳密な検証や 不正検知 ログ保管が必要
- 大量トラフィックでスパムが多く SLAが要求される
- フォームの仕様が頻繁に変わり 型やバリデーションが複雑
要するに「中小規模で確実に動くフォームを早く作る」には強い。一方で「プロダクトの核になるフォーム」には専用のバックエンドやフォームサービスを検討した方が安全です。
最短ルート これだけ読めば今日中に動く
最短で動かすために、先に全体の流れを固定します。ここが曖昧だと途中で迷子になります。
- Googleフォームを作る(項目は 名前 メール 内容)
- 回答先スプレッドシートを作る(自動で作成される)
- entry ID を取得して 自作フォームの hidden に割り当てる
- 入力ページ(index.html)で入力した値を sessionStorage に保存
- 確認ページ(confirm.html)で表示し hidden に詰め直して Googleフォームへ送信
- 完了ページ(complete.html)へ遷移する(送信後の見せ方を安定させる)
- 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 の見つけ方(おすすめ手順)
- Googleフォームのプレビューを開く
- ブラウザの開発者ツールを開く
- 要素を調べるで input や textarea を選ぶ
- 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("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
};
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"
});
}
トリガー設定手順(ここで止まりやすい)
- Apps Script 画面で トリガー を開く
- 関数を onFormSubmit にする
- イベントのソースを フォーム送信元のスプレッドシート にする
- イベントタイプを フォーム送信時 にする
- 保存して 権限を承認する
権限承認は初回に必ず出ます。ここで戻ると永遠に動きません。地味ですが最大のつまずきポイントです。
メリット この構成が選ばれる理由
- バックエンド不要で運用開始が早い
- 回答が自動でスプレッドシートに溜まるので管理が楽
- 確認画面と完了画面で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トリガーがフォーム送信時になっているか
- 権限承認が完了しているか
- 問い合わせ文の改行が確認画面で潰れていないか


