こんにちは、Hodaです。
今回は 僕が最近感動している、 「Netlify Forms」の使い方についてご紹介したいと思います。
今まで、WordPressのフォームや外部フォーム(Googleフォームやフォームラン)などを使っていた方も多いと思いますが、Netlifyでホスティングしているサイトであれば、そのまま Netlify Forms というものを利用することができます。
HTMLフォームに属性を1つ足すだけで、バックエンドなしで送信内容を受け取れるのでお問い合わせフォーム等にとても最適な機能です。
この記事では、Netlify Formsの基本的な使い方 から、JavaScriptでレンダリングするフォームへの対応、ファイルアップロード、送信通知、そして応用編としてGoogle Apps Script(GAS)と連携してGoogleスプレッドシートに自動で記録する方法まで、実務でよく使う範囲をまとめて解説します。
無料プランで十分に活用できる!
Netlify Formsとは
Netlify Formsは、デプロイ時にNetlifyのビルドシステムがHTMLを自動で解析し、フォームを検出してくれる仕組みです。APIコールを自分で書いたり、追加のJavaScriptを用意したりする必要はありません。

流れはシンプルです。
- サイトでフォーム検出(Form detection)を有効にする。
- HTMLの
<form>タグにnetlify属性(またはdata-netlify="true")を付ける - デプロイすると、Netlifyの管理画面から送信内容を確認できるようになる
フォーム検出を有効にする
まずはサイト側でフォームを検出できる状態にしておく必要があります。
フォーム検出を有効にする手順
- Netlifyの管理画面で対象サイトを開き、「Forms」タブに移動
- 「Enable form detection」を選択

次回のデプロイから、Netlifyが自動でHTML内のフォームをスキャンするようになります。
フォーム検出を無効にする手順
フォームをもう使わない場合や、別の方法で送信を処理したい場合は無効化もできます。
- 「Forms > Usage and configuration > Form detection」に移動
- 「Disable form detection」を選択
- 確認のためサイト名を入力し、再度「Disable form detection」を選択
無効化するとポスト処理が減るため、デプロイが多少速くなることがあります。ただし、既存のフォームを使い続ける予定がある場合は無効化しないよう注意してください。
HTMLフォームの作り方
フォーム検出を有効にしたら、<form>タグにnetlify属性またはdata-netlify="true"属性を追加します。
<form name="contact" method="POST" data-netlify="true">
<p>
<label>お名前: <input type="text" name="name" /></label>
</p>
<p>
<label>メールアドレス: <input type="email" name="email" /></label>
</p>
<p>
<label>お問い合わせ内容: <textarea name="message"></textarea></label>
</p>
<p>
<button type="submit">送信</button>
</p>
</form>
ポイントは以下の2つです。
name属性がNetlifyの管理画面に表示されるフォーム名になる- 1つのサイトに複数フォームがある場合は、
nameをそれぞれ変える
デプロイすると、Netlifyがビルド時にnetlify属性を取り除き、代わりにform-nameという隠しフィールドを自動で挿入してくれます。
AJAXで送信する場合
ページ遷移をさせずに送信したい場合は、fetchを使ってAJAX送信も可能です。
const handleSubmit = event => {
event.preventDefault();
const myForm = event.target;
const formData = new FormData(myForm);
fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData).toString()
})
.then(() => console.log("送信完了"))
.catch(error => alert(error));
};
document.querySelector("form").addEventListener("submit", handleSubmit);
送信データはURLエンコードする必要があります。またNetlify Formsは現時点でJSON形式の送信には対応していない点も覚えておいてください。
React・Vue・Next.jsなどでレンダリングするフォームへの対応
NetlifyのビルドシステムはデプロイされたHTMLを解析してフォームを見つけるため、クライアントサイドでJavaScriptによって描画されるフォームは、そのままではビルド時に検出されません。
対応方法は2つあります。
- 検出用の非表示HTMLフォーム(
data-netlify="true"属性付き)を別に用意し、実際のフォームと同じnameのinputを揃えておく - JavaScriptで描画するフォームの中に、
form-nameという隠しinputを追加する
<input type="hidden" name="form-name" value="pizzaOrder" />
Next.jsを使っている場合は、バージョンによって挙動が変わることがあるため、フレームワークごとの設定ガイドも合わせて確認しておくと安心です。
送信後の挙動をカスタマイズする
デフォルトでは、送信後に汎用的な成功メッセージが表示されるページへリダイレクトされます。
- カスタム成功ページ:
<form>タグにaction属性を追加し、任意のページパス(例:/thanks)を指定する - カスタムアラート:AJAXで送信している場合は、リダイレクトの代わりに
alert()などで完了メッセージを表示することも可能
ファイルアップロードに対応する
type="file"のinputを追加すれば、ファイル付きの送信も受け取れます。
<form name="fileForm" enctype="multipart/form-data" data-netlify="true">
<label>お名前: <input name="name" type="text" /></label>
<label>ファイル添付: <input name="file" type="file" /></label>
<button>送信</button>
</form>
| 制限事項 | 内容 |
|---|---|
| 1フィールドあたりのファイル数 | 1つまで(複数必要な場合はフィールドを分ける) |
| リクエストの最大サイズ | 8MB |
| アップロードのタイムアウト | 30秒 |
個人情報を含むファイルをアップロードさせる場合は、標準のフォーム機能だけに頼らず、追加のセキュリティ対策(Very Good Securityとの連携など)を検討することが推奨されています。
送信データの管理
送信されたデータは「Forms」タブの各フォームから確認できます。
- 認証済み(verified)の送信のみがデフォルトで表示され、スパム扱いのものは別枠で確認可能
- 「Download as CSV」でCSVエクスポートができる
- スパム⇔認証済みのステータス変更や、送信の削除も管理画面から可能
<script>タグなど悪意のあるコードが送信された場合は自動でサニタイズ(無害化)される
フォームや送信を削除すると元に戻せないため、必要なデータは事前にCSVでバックアップしておくのが安全です。
通知を設定する
新しい送信があるたびに気づけるよう、通知を設定しておくと便利です。
- 対象サイトの「Configuration > Notifications > Form submission notifications」に移動
- 「Add notification」を選択し、メール通知や外部サービスへのHTTP POST(Outgoing webhook)を設定
この「Outgoing webhook」こそが、次に紹介するGAS連携のポイントになります。
無料プランで十分に活用できる!
応用編:GASでGoogleスプレッドシートに自動保存する
Netlify Formsの管理画面だけでも十分運用できますが、僕自身は複数の特典請求・問い合わせフォームをまとめて管理したいので、送信内容をGoogle Apps Script(GAS)のWebhookで受け取り、Googleスプレッドシートに自動で追記する仕組みを組んでいます。
全体の流れ
- NetlifyのForm submission notificationsで「Outgoing webhook」を設定し、送信先URLにGASのWebアプリURLを指定
- GAS側でリクエストを受け取り、共有シークレットを照合
- フォーム名に応じて、対応するスプレッドシートのシートに1行追記
GASのサンプルコード
フォーム名やフィールド名は汎用的な例に置き換えています。構成自体はそのまま使い回せます。Claude やChatGPTをお使いの場合、以下をそのまま参考データとして使っていただければ、理解してくれると思います
/**
* フォーム送信 → Googleスプレッドシート追記
*
* 対応フォーム:
* - contact-form → contact タブ
* - download-request → download-request タブ
* - newsletter-signup → newsletter タブ
*
* スクリプトプロパティ:
* WEBHOOK_SECRET(必須)
* SHEET_NAME_CONTACT / SHEET_NAME_DOWNLOAD / SHEET_NAME_NEWSLETTER(任意)
*/
var FORM_CONFIG = {
'contact-form': {
sheetProp: 'SHEET_NAME_CONTACT',
defaultSheet: 'contact',
headers: ['受信日時', 'お名前', 'メールアドレス', 'お問い合わせ内容'],
buildRow: function (data, nowJst) {
return [
data.receivedAt || nowJst,
data.name || '',
data.email || '',
data.message || ''
];
}
},
'download-request': {
sheetProp: 'SHEET_NAME_DOWNLOAD',
defaultSheet: 'download-request',
headers: ['受信日時', 'メールアドレス', '希望資料'],
buildRow: function (data, nowJst) {
return [
data.receivedAt || nowJst,
data.email || '',
data.material || ''
];
}
},
'newsletter-signup': {
sheetProp: 'SHEET_NAME_NEWSLETTER',
defaultSheet: 'newsletter',
headers: ['受信日時', 'メールアドレス'],
buildRow: function (data, nowJst) {
return [
data.receivedAt || nowJst,
data.email || ''
];
}
}
};
function doPost(e) {
var lock = LockService.getScriptLock();
try {
lock.waitLock(15000);
} catch (lockErr) {
return jsonResponse({ ok: false, error: 'lock_timeout' });
}
try {
if (!e.postData || !e.postData.contents) {
return jsonResponse({ ok: false, error: 'empty_body' });
}
var data = JSON.parse(e.postData.contents);
var props = PropertiesService.getScriptProperties();
var expected = props.getProperty('WEBHOOK_SECRET');
if (!expected || data.secret !== expected) {
return jsonResponse({ ok: false, error: 'forbidden' });
}
var form = data.form;
var config = FORM_CONFIG[form];
if (!config) {
return jsonResponse({ ok: false, error: 'unknown_form: ' + form });
}
var sheetName = props.getProperty(config.sheetProp) || config.defaultSheet;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
return jsonResponse({ ok: false, error: 'sheet_not_found: ' + sheetName });
}
var nowJst = formatNowJst();
var row = config.buildRow(data, nowJst);
sheet.appendRow(row);
return jsonResponse({ ok: true, sheet: sheetName });
} catch (err) {
return jsonResponse({ ok: false, error: String(err.message || err) });
} finally {
lock.releaseLock();
}
}
/** 日本時間 ISO風(+09:00) */
function formatNowJst() {
return Utilities.formatDate(new Date(), 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss.SSS") + '+09:00';
}
function jsonResponse(obj) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
/** 初回セットアップ:各タブにヘッダー行を書く(メニューから実行) */
function setupSheetHeaders() {
var props = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.getActiveSpreadsheet();
Object.keys(FORM_CONFIG).forEach(function (formKey) {
var config = FORM_CONFIG[formKey];
var sheetName = props.getProperty(config.sheetProp) || config.defaultSheet;
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
}
if (sheet.getLastRow() === 0) {
sheet.appendRow(config.headers);
sheet.getRange(1, 1, 1, config.headers.length).setFontWeight('bold');
}
});
SpreadsheetApp.getUi().alert('ヘッダー行のセットアップが完了しました。');
}
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('form-webhook')
.addItem('ヘッダー行を作成', 'setupSheetHeaders')
.addToUi();
}
そうすると、以下のようにしっかりとGoogleシートの新しい行に保存されるようになります!

設定のポイント
WEBHOOK_SECRETは必ずスクリプトプロパティに保存する。コード内にベタ書きしないことで、スクリプトを共有・複製しても秘密情報が漏れません(この考え方は前回の環境変数の記事と同じです)- フォームが増えても
FORM_CONFIGにエントリを追加するだけで対応できる設計にしてあるので、運用しながらフォームを増やしやすい setupSheetHeadersをメニューから一度実行しておけば、シートのヘッダー行を毎回手動で用意する必要がないLockServiceで排他制御しているのは、短時間に複数の送信が重なった時に書き込みが競合するのを防ぐため
この方法のメリット・デメリット
| 内容 | |
|---|---|
| メリット | 複数フォームの送信内容を1つのスプレッドシートに集約できる。他のツールとの連携がしやすい。Netlifyの管理画面を開かなくても一覧で確認できる |
| デメリット | GAS側の実装・保守が必要になる。Webhookの秘密鍵管理など、セキュリティ面の設定を自分で行う必要がある |
フォームの数が少なく、単純な問い合わせ対応で十分な場合は、無理にGASを使わずNetlifyの管理画面だけで運用する方がシンプルです。フォームの種類や件数が増えてきたタイミングで、この方法を検討してみるとよいでしょう。
フォーム送信をFunctionsのトリガーにする
Netlify Formsは、Netlify Functionsとも標準で連携できます。フォームの送信が検証されたタイミングでsubmission-createdという名前のFunctionを自動的に呼び出せるため、GASを使わずにNetlifyの中だけで後続処理(通知、外部APIへの連携など)を完結させることも可能です。GASでの外部連携と、Functionsでの内製処理は、要件に応じて使い分けるとよいでしょう。
まとめ
- Netlify Formsは、HTMLに属性を1つ足すだけでバックエンド不要のフォーム送信を実現できる機能
- JavaScriptでレンダリングするフォームは、隠しフォームか
form-nameの隠しinputで対応する - ファイルアップロードには容量・件数・タイムアウトの制限があるため、要件に合わせて確認しておく
- 送信件数が増えてきたら、GAS連携やFunctionsトリガーで自分の運用に合わせた仕組みを組むと管理がぐっと楽になる
Netlify自体の基本機能や料金についてはNetlifyの特徴と料金を徹底解説、環境変数の設定方法についてはNetlifyの環境変数設定ガイド完全版もあわせてご覧ください。
無料プランで十分に活用できる!


