# フォーム

> data-sitekit-form を付けるだけのフォーム。表示・通知・自動返信・コンバージョンの計測（GA4・Google 広告・Meta）・テスト送信・流入元の記録。近日中に、テンプレート・公開ページ・表示の条件・重複防止・定員とキャンセル待ち・集計と CSV。

URL: https://siteon.dev/ja/docs/forms

Siteon は、AI で作ったサイトのためのインフラです。AI や手で作った静的サイト（HTML）に、ホスティング・お客さん（お店の人）が直せる CMS・お問い合わせフォーム・アクセス解析・AI エージェントからの操作（CLI・MCP）を足し、そのまま納品できる状態にします。

## 最小の形

項目だけ書きます。送信処理・hidden 項目・エラー表示の JavaScript は要りません（[script タグ](https://siteon.dev/ja/docs/script) は入れておきます）。

```html
<form data-sitekit-form="contact">
  <label>お名前 <input name="name" required autocomplete="name"></label>
  <label>メール <input type="email" name="email" required autocomplete="email"></label>
  <label>お問い合わせ内容 <textarea name="message"></textarea></label>

  <!-- ボット対策。人には見えない場所に置く（必須） -->
  <input type="text" name="_hp" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px" aria-hidden="true">

  <button type="submit">送信する</button>
</form>

<div data-sitekit-success role="status" aria-live="polite" hidden>お問い合わせありがとうございました。</div>
<div data-sitekit-error role="status" aria-live="polite" hidden></div>
```

## 名前を合わせる

| HTML | schema.json |
|---|---|
| `data-sitekit-form="contact"` の `contact` | `forms[].key` |
| 入力の `name="email"` | その form の `fields[].key` |

- schema.json に無いフォームのキーは `not_found` になります
- schema.json に無い `name` はエラーにならず、保存もされません
- `required` の項目は、空だと `invalid_request` になります（項目ごとの文字数などの制約は [schema.json の型](https://siteon.dev/ja/docs/schema#型)）

## ハニーポット（`_hp`）は必ず入れる

`name="_hp"` の入力は、人には見えないボット対策の欄です。**ここが埋まっている送信は保存されません**。ただしボットに気づかせないため、画面上は成功と同じに見えます。

- `tabindex="-1"` と `autocomplete="off"` を付け、画面の外に置きます（ブラウザの自動入力で埋まらないように）
- schema.json には書きません

## 成功・エラーの表示

`[data-sitekit-success]` と `[data-sitekit-error]` は、**フォームの中か、フォームと同じ親要素の直下**に置きます。

- 成功するとフォームが `hidden` になります。**成功の文言はフォームの外に置きます**（中に置くと一緒に隠れる）
- `role="status" aria-live="polite"` を付けます。付けないと、送信後に出た文言がスクリーンリーダーに読み上げられません
- エラーのときは `[data-sitekit-error]` に次の文言が入ります

| 状況 | 表示される文言 |
|---|---|
| 入力の誤り（`invalid_request`） | 入力内容をご確認ください |
| 短時間に送りすぎ（`rate_limited`） | しばらく経ってからお試しください |
| Turnstile の確認に失敗（`turnstile_failed`） | 確認に失敗しました。もう一度お試しください |
| 通信できない | 送信できませんでした。通信環境をご確認ください |
| そのほか | 送信できませんでした。時間をおいてお試しください |

重複回答の防止・定員つきのフォームでは、次も出ます。

| 状況（`error`） | 表示される文言 |
|---|---|
| 回答済み（`already_submitted`） | 回答済みです |
| 満席（`full`） （近日）| 満席です |
| 受付期間外（`not_open` / `closed`） （近日）| 受付期間外です |

入力の誤りのときは、該当する項目に `aria-invalid="true"` が付きます。見た目はサイト側の CSS で当てます。

```css
[aria-invalid="true"] { border-color: #c00; }
```

## 送信後に別ページへ移動する

```html
<form data-sitekit-form="contact" data-success-url="/thanks/">
```

**同じオリジンのページだけ**です。別のオリジンへ移動するときは、明示して許可します。

```html
<form data-sitekit-form="reserve"
      data-success-url="https://booking.example.com/done"
      data-success-external="1">
```

スキームやポートが違えば別オリジンです。`javascript:` と `data:` は `data-success-external` を付けても移動しません。

## 二重送信の防止

連打や、入力欄での Enter による再送信は自動で止めます。送信中は送信ボタンが `disabled` になり、`<form>` に `aria-busy="true"` が付きます。`form="フォームのid"` でフォームの外に置いた送信ボタンにも効きます。

## ファイルの添付

`<input type="file" name="photo" accept="image/jpeg,image/png,image/webp,application/pdf">` を置くと、写真や PDF を受け取れます。管理画面でフォームに「ファイル」の項目を足すと、埋め込みコードにもこの欄が出ます。

- 受け取れるのは画像（JPEG・PNG・WebP）と PDF。1 ファイル 10MB、1 回の送信で 3 ファイル・合計 20MB まで。サイトの写真容量に含まれます
- 1 つの項目に付けられるファイルは 1 つです（`multiple` や同じ名前の欄を並べると、送信を止めて知らせます）
- 届いたファイルは、管理画面の受付の詳細からダウンロードします（リンクは 15 分で切れます）。通知メールにはファイルを付けず、「添付ファイルあり」と管理画面へのリンクを送ります。Webhook・CSV・スプレッドシートにもファイルは出ません
- 送信に失敗したとき（大きすぎる・形式が違うなど）は、送信を止めて理由を表示します。ファイルが黙って捨てられることはありません
- 予約ページのフォームには、いまはファイルの項目を付けられません
- 埋め込みスクリプトを自分のサイトにコピーして置いている場合は、新しい版に差し替えてください。古い版のままだと、ファイルの項目があるフォームは送信できません

## 複数の値を持つ項目

チェックボックスの群や `<select multiple>` など、**同じ `name` を複数使うと最後の値だけ**が送られます。複数の答えが要る質問は、キーを分けます。

```html
<!-- NG: topic が1つしか届かない -->
<input type="checkbox" name="topic" value="a">
<input type="checkbox" name="topic" value="b">

<!-- OK -->
<input type="checkbox" name="topic_a" value="1">
<input type="checkbox" name="topic_b" value="1">
```

### チェックボックスの群（`checkbox_group`）

同じ `name` のチェックボックスを、チェックされた値が**全部**届く形にできます。schema では項目の type を `checkbox_group` にし、`options` に選択肢を書きます。

```html
<label><input type="checkbox" name="topic" value="price"> 料金</label>
<label><input type="checkbox" name="topic" value="hours"> 営業時間</label>
```

`checkbox_group` 以外の項目に同じ `name` を複数置くと、最後の値だけが保存されます。

## Turnstile

Cloudflare Turnstile のウィジェットが作る `cf-turnstile-response` の入力があれば、自動で一緒に送ります。サイト側で特別な記述は要りません。

## 通知

お問い合わせは**保存された時点で確定**します。通知はそのあとに送られ、どの通知が失敗してもお問い合わせは消えません。各通知の成否は管理画面のお問い合わせの詳細に出て、失敗したものは再送できます。

通知先は schema.json ではなく、**管理画面の「設定 → 通知 → 各フォーム」**で、サイトのオーナーが変えます。

| 通知 | 設定するもの |
|---|---|
| お店へのメール | お知らせ先のメールアドレス（複数可） |
| Slack | Incoming Webhook の URL（Slack でアプリを作り、投稿先チャンネルを選んで発行する） |
| Webhook（有料プラン） | 送り先の https の URL。管理画面の「設定 → 詳細 → Webhook」で登録し、Zapier・Make・CRM などへ署名付きの JSON を送ります（[Webhook](https://siteon.dev/ja/docs/webhooks)） |
| Google スプレッドシートへの記録 （近日）| 各自の Google アカウントでつなぎます。まれに同じ回答が 2 行入ることがあり、先頭列の ID で見分けられます |

- 通知ごとにオン・オフを切り替えられます
- Slack の Webhook URL は、持っている人が誰でもそのチャンネルに投稿できる鍵です。管理画面は伏せ字で表示し、API も返しません。URL を失くしたら Slack で発行し直します
- 設定を変えられるのはオーナーだけです（スタッフは見るだけ）

## お礼メール（自動返信）と差し込み変数

お客様へのお礼メールは、次のときに送られます。

1. 管理画面の「設定 → 通知 → 各フォーム」で、そのフォームの「お礼メール」がオン
2. そのフォームに `type: "email"` の項目があり、値が入っている

件名と本文は「設定 → 通知 → 各フォーム」で書きます（プレビューあり）。差し込める変数:

| 変数 | 中身 |
|---|---|
| `{{name}}` `{{email}}` など | フォームの項目の値（項目の key そのまま） |
| `{{site_name}}` | サイト名 |
| `{{form_label}}` | フォームの名前（`forms[].label`） |
| `{{submitted_at}}` | 送信日時 |

```text
{{name}} 様

{{site_name}} へのお問い合わせありがとうございます。
内容を確認のうえ、ご連絡いたします。

受付日時: {{submitted_at}}
```

## 送信を GTM で拾う

送信が**成功したときだけ**（受け付けて保存し終えたあとに1回）、`dataLayer` に次が積まれます。送信ボタンを押した時点や、通信に失敗したときには積まれません。

```js
{
  event: "sitekit_form_submit",     // 完了イベント名（フォームごとに変えられる）
  form: "contact",                  // フォームのキー（data-sitekit-form の値）
  sitekit_form: "contact",          // 同じ値（form と同じ）
  sitekit_site: "hanaya",           // サイト ID（data-site）
  sitekit_submission_id: "…"        // 受付 ID。コンバージョンの重複を除くのに使う
}
```

- **入力内容（名前・メールアドレスなど）は入れません。** 広告や解析のタグは受け取った値を外部に送るので、個人情報を載せないためです
- イベント名は管理画面の「設定 → 通知 → 各フォーム」の完了イベント名で変えられます。既定は `sitekit_form_submit` です。名前は送信の応答で届くので、サイトの再デプロイは要りません。**名前を変えたら GTM のトリガーも直します**（古い名前のトリガーは黙って動かなくなります）
- ハニーポットで弾いた送信も画面上は成功なので、このイベントは積まれます（このとき `sitekit_submission_id` は `null`）。実際に届いた件数は管理画面の一覧で数えます

### GTM を使わない場合（`sitekit:submit` イベント）

同じ内容を `detail` に持つ DOM イベント `sitekit:submit` が `document` に送られます。GA4 や Meta のタグを自分で書くサイトはこちらを拾います。

```html
<script>
  document.addEventListener("sitekit:submit", (e) => {
    const d = e.detail; // { event, form, sitekit_form, sitekit_site, sitekit_submission_id }
    gtag("event", "generate_lead", { form_key: d.sitekit_form, transaction_id: d.sitekit_submission_id ?? undefined });
    fbq("track", "Lead", { content_name: d.sitekit_form }, { eventID: d.sitekit_submission_id ?? undefined });
  });
</script>
```

### GTM の設定

1. **トリガー** — 種類は「カスタム イベント」、イベント名は `sitekit_form_submit`（管理画面で変えたならその名前）。フォームで分けたいときは「一部のカスタム イベント」で、下の変数 `Form Key` が `contact` に等しい、のように条件を付けます
2. **変数** — 種類は「データレイヤーの変数」。`form`（名前は `Form Key` など）と、重複を除くなら `sitekit_submission_id`（名前は `Submission ID` など）を作ります
3. **タグ** — 送りたいタグの配信トリガーに 1 を指定します（下の例）

### 例: GA4 のキーイベント

| 項目 | 値 |
|---|---|
| タグの種類 | Google アナリティクス: GA4 イベント |
| 測定 ID | `G-XXXXXXX`（GA4 の設定タグと同じ） |
| イベント名 | `generate_lead`（GA4 の推奨イベント名。ほかの名前でもよい） |
| イベント パラメータ | `form_key` = `{{Form Key}}` |
| 配信トリガー | 上のトリガー |

GA4 の管理画面の「イベント」で `generate_lead` を**キーイベントとしてマーク**すると、コンバージョンとして数えられます。`form_key` を付けておくと、どのフォームからの問い合わせかを GA4 で分けられます。

### 例: Google 広告のコンバージョン（トランザクション ID）

| 項目 | 値 |
|---|---|
| タグの種類 | Google 広告のコンバージョン トラッキング |
| コンバージョン ID / ラベル | Google 広告で「コンバージョン アクション」（カテゴリは「リードフォームの送信」など）を作ると表示される値 |
| トランザクション ID | `{{Submission ID}}`（同じ送信が二重に数えられるのを防ぐ） |
| 配信トリガー | 上のトリガー |

同じコンテナに「コンバージョン リンカー」タグ（すべてのページで配信）が無ければ足します。Meta のピクセルで重複を除くときは、`eventID` に同じ受付 ID を渡します（上の `sitekit:submit` の例）。`gclid` は script が流入元として別に記録しているので、オフラインのコンバージョンの取り込みに使うなら、管理画面の CSV か [Webhook](https://siteon.dev/ja/docs/webhooks) で読めます。

### 確かめ方

GTM のプレビュー（Tag Assistant）でサイトを開き、フォームを実際に送ります。左のイベントの一覧に完了イベントが出て、「Data Layer」タブに `form` と `sitekit_submission_id` が入っていれば設定できています。

## テスト送信

いちばん確かなのは、公開したページ（開発用ドメインでもよい）から実際に送ることです。API に直接送ることもできます。

```bash
curl -s -X POST -H 'content-type: application/json' \
  -d '{"name":"テスト","email":"test@example.com","message":"テスト送信です"}' \
  https://cdn.siteon.dev/v1/sites/hanaya/forms/contact
# => {"ok":true,"id":"...","gtm_event":"sitekit_form_submit"}
```

**テストでも本物のお問い合わせとして保存され、通知も届きます。** 件数の上限にも数えられます。試したあとは管理画面から消してください。CLI の `siteon forms test`（MCP の `send_test`）でも送れます（[CLI・MCP・API キー](https://siteon.dev/ja/docs/cli-mcp)）。

## 流入元（UTM）の記録

script が覚えておいた流入元（最初に来たときと最後に来たときの `utm_*` / `gclid` / `fbclid`・着地ページ・参照元）が、送信のたびに自動で添えられ、お問い合わせと一緒に保存されます。サイト側の作業はありません（[script タグ](https://siteon.dev/ja/docs/script#流入元utmの記録)）。

## テンプレートから作る

よくある形は、項目と設定の入ったテンプレートから作れます。管理画面の「フォーム → テンプレートから追加」、CLI の `siteon forms create --template <名前>`、MCP の `create_form` のどれでも同じものができます。

| テンプレート | 中身 |
|---|---|
| `contact` | お問い合わせ（名前・メール・電話・内容） |
| `survey` （近日）| アンケート。満足度（5 段階）・NPS・良かった点（複数選択）・自由記述。個人情報の項目を含まない設定（下の「集計と CSV」） |
| `application` （近日）| 申込。希望日時ごとの定員とキャンセル待ち、メール確認つき（下の「定員」「重複回答の防止」） |
| `brochure` | 資料請求（会社名・連絡先・欲しい資料） |
| `job` | 求人応募（連絡先・希望職種・志望理由） |

作ったあとは、項目も設定も普通のフォームと同じように直せます。

`survey` は同じブラウザでの重複防止を、`application` は定員とメール確認を使うので、この 2 つで作ったフォームは、それらの機能が開くまで受け付けません （近日）（`survey` は `dedupe` を `none` か `phone` に変えれば受け付けます）。

## 公開ページ（サイトが無くても使える）

フォームごとに、Siteon がホストする公開ページを出せます（管理画面「フォーム → 公開」、`siteon forms publish <form>`）。

- URL は `https://<共通ドメイン>/f/<ページ ID>`。QR コード（SVG）と、ほかのサイトに貼る埋め込みコード（iframe）も出ます
- サイトの公開（deploy）は要りません。Free プランのページには、小さな「Made with Siteon」のバッジが付きます
- 送信は Cloudflare Turnstile で守られ、JavaScript が無くても送れます
- 止めると、ページも受付も止まります（もう届いた申込の確認・取消のリンクは動きます）
- 決済に使う語（カード番号・暗証番号など）を含むフォームは、Free プランでは保存できず、有料プランでは公開の前に自動の検査に回ります（フォームでカード情報を集めないでください）

## 表示の条件（`show_if`）

「〇〇を選んだときだけ出す」項目は、schema の項目に `show_if` を書きます。参照できるのは同じフォームの select / radio / checkbox_group / checkbox で、連鎖は 3 段までです。

| `op` | 参照先 | 意味 |
|---|---|---|
| `eq` / `neq` | select・radio | 値が `value` と同じ / 違う |
| `in` | select・radio・checkbox_group | 値のどれかが `value`（配列）に入っている |
| `not_empty` | select・radio・checkbox_group | 何か選ばれている |
| `checked` | checkbox（`value` なし）・checkbox_group（`value` 1 つ） | チェックされている |

```json
{ "key": "detail", "label": "詳しく", "type": "textarea", "show_if": { "field": "topic", "op": "eq", "value": "other" } }
```

条件に合わない項目は、`required` でも空で送れ、値を送っても保存されません（判定はサーバーが行います）。
公開ページでは自動で隠れます。自分のサイトに埋め込むときは、項目を次の枠で包むと script が隠します（隠れた項目は送られず、`required` でも止まりません）。

```html
<div data-sitekit-field="detail" data-sitekit-show-if='{"field":"topic","op":"eq","value":"other"}'>
  <label>詳しく <textarea name="detail" required></textarea></label>
</div>
```

管理画面「サイトに組み込む」が出すフォームの HTML と AI 向けの指示文には、この枠が入っています。

## 重複回答の防止（`dedupe`）

同じ人の 2 回目を止める方法を、フォームごとに選べます。

| `dedupe` | 何で同じ人とみなすか | 限界 |
|---|---|---|
| `none`（既定） | 止めない（通信の再送による二重送信だけ止まる） | 同じ人の 2 回目は 2 件 |
| `email_verify` （近日）| メールアドレス（Gmail の別名は同じとみなす）。**確認メールのリンクを開いて確定した回答だけ**が有効 | 別のアドレスを持てば別人 |
| `browser` （近日）| 同じブラウザ（公開ページは cookie、埋め込みは script が `localStorage` に置く値） | 別のブラウザ・シークレットウィンドウ・履歴の消去で回避できる。保存を拒むブラウザでは止まらない |
| `phone` | 電話番号（数字だけにそろえる。`+81` は `0`） | 番号の持ち主は確かめない（補助にとどまる） |

- `email_verify` と `browser` の受付は準備中です （近日）（いまは `none` と `phone` が使えます）
- 2 回目は 409 `already_submitted`（script は「回答済みです」と出す）
- `email_verify`: 送信すると確認メールが届き、**24 時間以内**（定員つきは `confirm_until` まで）にリンクを開いて「確定する」を押すと有効になります。確認前に同じアドレスで送ると、確認メールを送り直します（1 時間 3 通まで）。期限が切れた回答は数えず、同じアドレスでまた申し込めます
- 判定に使う値は鍵つきのハッシュでだけ保存し、元の cookie の値などは持ちません

## 定員・キャンセル待ち（`capacity`） （近日）

申込フォーム（`kind: "application"`）に定員を付けられます。

| キー | 意味 |
|---|---|
| `limit` | 定員（1〜10,000）。必須 |
| `waitlist` | キャンセル待ちの枠（0〜10,000、既定 0） |
| `by_field` / `limits` | 選択肢ごとの定員（例: 希望日時ごと）。`by_field` は必須の select / radio。`limits` で選択肢ごとに別の数にできる |
| `opens_at` / `closes_at` | 受付の期間（タイムゾーンつきの ISO 8601。例 `2026-11-01T10:00:00+09:00`） |
| `confirm_until` | メール確認の期限（`email_verify` のとき） |
| `cancel_until` | お客様が取り消せる期限 |
| `event_at` / `remind_before_hours` | 当日の日時と、何時間前にリマインドを送るか（1〜168） |

- 席があれば確定、満席ならキャンセル待ち、待ちも一杯なら 409 `full`。期間外は 409 `not_open` / `closed`
- 同時に送られても定員は超えません
- 応答は `{"ok": true, "state": "confirmed"}` か `{"ok": true, "state": "waitlisted", "ahead": 2}`（前に待っている人数）。script は、キャンセル待ちと確認待ちのときは送信後のページへ移らず、成功の表示に「キャンセル待ちです（前に 2 人）」「確認メールを送りました」と出します
- 自動返信の設定に関係なく、受付票のメール（状態・受付番号・取消のリンクつき）が届きます。取消のリンクから、`cancel_until` まで本人が取り消せます
- 確定の席が取り消されると、キャンセル待ちの先頭が**同じ処理で**確定に繰り上がり、その人にメールが届きます
- 店は管理画面「お問い合わせ」で、各申込の状態（確定・キャンセル待ち（N 番目）・確認待ちなど）を見て、「申込を取り消す」で取り消せます（CLI `siteon submissions cancel`、MCP `cancel_submission` も同じ）
- 満席になると、アカウントのオーナーに一度だけお知らせが届きます。公開ページは `page.show_remaining` で残席を出せます（選択肢ごとの定員では出ません）

## 集計と CSV

- 選択肢（select・radio・checkbox_group）・評価（`rating`、既定 1〜5）・NPS（`nps`、0〜10）を、受け付けるたびに数えます。管理画面「フォーム → 集計・CSV」、CLI `siteon forms stats <form>`、MCP `get_form_stats`。NPS は推奨（9–10）の割合 − 批判（0–6）の割合
- CSV はサイトのオーナーだけ（管理画面、CLI `siteon forms export <form>`）。Excel でそのまま開ける BOM つき UTF-8。列は id・日時・状態・担当・申込の状態・枠・版・各項目・メモ数。1 回 50,000 件まで（多いときは期間で分ける）。書き出しは履歴に残ります
- 回答を消しても集計の数は戻りません（CSV は消した後の回答だけ）
- `no_personal_fields: true` の個人情報の項目を含まないアンケートは、メール・電話の項目を置けず、ブラウザの情報・流入元を保存せず、時刻は時間単位で持ちます。回答の本文に書かれたことは残ります
- `emit_submission_id: false` にすると、計測（dataLayer・`sitekit:submit`）に回答の id を出しません

## 受付の上限

フォームの受付件数はプランごとに月の上限があります（予約もフォームの受付に含めて数えます）。上限に達したときの動きは [プランと上限](https://siteon.dev/ja/docs/plans#上限に達したとき) にあります。
