# 公開 API

> ログイン不要で使える2つのエンドポイント（公開中の内容の JSON・フォームの受付）の仕様とレスポンス例。

URL: https://sitekit.example/docs/api

## エンドポイント

ふだんは script と中継が呼ぶので、直接使う必要はありません。自前で描画したいとき、サーバーから送りたいときに使います。

| メソッド | パス | 中身 |
|---|---|---|
| `GET` | `/v1/sites/<site>/content?locale=<言語>` | 公開中の内容（JSON） |
| `POST` | `/v1/sites/<site>/forms/<form>` | フォームの受付 |
| `GET` | `/sitekit.js` | サイトに入れる script |

オリジンは `https://cdn.sitekit.example` です（script の `src` と同じ）。

管理用の API（サイトの作成・schema の反映・お問い合わせの一覧）は、API キーとあわせて準備中です （近日）（[CLI・MCP・API キー](https://sitekit.example/docs/cli-mcp)）。

## 公開中の内容（content）

```bash
curl -s "https://cdn.sitekit.example/v1/sites/hanaya/content?locale=ja"
```

- `locale` を省略すると主言語。schema.json の `locales` に無い言語は `400 {"ok":false,"error":"bad_request"}`
- 無いサイトは `404 {"ok":false,"error":"not_found"}`
- ログイン不要・読み取り専用で、`Access-Control-Allow-Origin: *`
- `cache-control: public, max-age=60`（公開してから最長1分で反映）
- 公開中の記事だけが載ります（下書きは載らない）

### レスポンスの例

```json
{
  "values": {
    "hours.days": "月〜金",
    "hours.time": "10:00 - 19:00"
  },
  "lists": {
    "news": [
      {
        "id": "a1b2c3d4e5f6g7h8i9j0",
        "title": "秋の新メニュー",
        "date": "2026-10-01",
        "tags": ["秋"],
        "category": "新商品",
        "category_label": "新しい商品",
        "category_color": "red",
        "thumbnail": "https://cdn.sitekit.example/a/hanaya/…",
        "body": [{ "type": "…" }]
      }
    ]
  },
  "tags": { "news": [{ "tag": "秋", "count": 3 }] },
  "tags_meta": { "news": { "秋": { "color": "gold" } } },
  "tracking": { "gtm_id": "GTM-XXXXXXX" }
}
```

| キー | 中身 |
|---|---|
| `values` | groups の値。キーは `<グループ>.<項目>`。翻訳が無い・空のときは主言語の値 |
| `lists` | collections の公開中の記事。1つの一覧に新しい順で最大 100 件を取り、schema.json の `sort`（無ければ管理画面の並び）で並べる |
| `image` の項目 | 写真の絶対 URL |
| `select` の項目 | 保存値に加えて `<項目>_label`、色のある選択肢だけ `<項目>_color` |
| `tags` | タグを持つ collection ごとの、使われているタグと件数（多い順） |
| `tags_meta` | 色を決めたタグの色。色の無いタグはキーごと無い |
| `tracking` | GTM ID（管理画面の設定 ＞ schema.json の順） |

記事の `url`（記事ページへのリンク）と、タグの `{tag, url, color}` への変換は、この JSON ではなく**中継が**足します。

## フォームの受付（forms）

```bash
curl -s -X POST -H 'content-type: application/json' \
  -d '{"name":"山田","email":"yamada@example.com","message":"予約できますか"}' \
  https://cdn.sitekit.example/v1/sites/hanaya/forms/contact
```

### 送るもの

- `application/json` か `application/x-www-form-urlencoded`。multipart は受け付けません
- 本文は 16KB まで
- 項目は schema.json の `forms[].fields` の key で。知らないキーは無視します
- 同じキーが複数あると最後の値が使われます

次の項目は schema.json に書かずに送れます。

| 項目 | 中身 |
|---|---|
| `_hp` | ハニーポット。空でない値があればボットとみなして保存しない |
| `idempotency_key` | 100 文字まで。同じキーの再送は、2件目を作らず元の受付を返す |
| `attribution` | 流入元。`{first, last}` の形（form-urlencoded では JSON 文字列）。500 文字を超える値は捨てる。形が壊れていても送信は失敗しない |
| `cf-turnstile-response` | Turnstile のトークン |

### 成功

| 状況 | 応答 |
|---|---|
| 受け付けた | `200 {"ok":true,"id":"…","gtm_event":"sitekit_form_submit"}` |
| ハニーポットに掛かった（保存しない） | `200 {"ok":true,"id":null,"gtm_event":"…"}`（ボットに気づかせないため成功と同じ形） |
| `idempotency_key` の再送 | `200 {"ok":true,"id":"<元の id>","duplicate":true,"gtm_event":"…"}` |
| form-urlencoded で送り、schema.json に `success_url` がある | `303` でその URL へ（サイトの許可オリジンの中だけ） |

`gtm_event` は管理画面で決めた完了イベント名です。

### エラー

すべて `{"ok":false,"error":"…"}` の形です。

| 状態 | `error` | 意味 |
|---|---|---|
| 400 | `invalid_request` | 入力の誤り。`errors` に誤りのある項目の key が並ぶ（値は返さない）。JSON として読めない本文もこれ |
| 403 | `turnstile_failed` | Turnstile のトークンが無い、または確認できない |
| 404 | `not_found` | サイトかフォームが無い |
| 413 | `payload_too_large` | 16KB を超えた |
| 429 | `rate_limited` | 同じ接続元から 10 分に 20 件を超えた |

### ブラウザから別のオリジンで送る場合（CORS）

ブラウザからの送信は、サイトの**許可オリジン**に送信元のオリジンが入っている必要があります。入っていないと CORS のヘッダが返らず、ブラウザが応答を読めません。`Origin` の無い送信（curl・サーバー）は許可オリジンに関係なく受け付けます。Cookie は使いません。

> 予定: 許可オリジンに無い `Origin` からの送信は、保存せず `403 origin_not_allowed` を返すように変わります（他人のサイトからフォームを使われて件数を消費されないように）。上限に達したときは `429 quota_exceeded` です（[プランと上限](https://sitekit.example/docs/plans#上限に達したとき)）。
