{
  "openapi": "3.0.3",
  "info": {
    "title": "ShareDanCMS 公開API",
    "description": "ShareDanCMSの公開API（閲覧専用）に関するAPIドキュメントです。\n\n各Webサイト（site単位のフロントエンド）へ、投稿・FAQ・固定コンテンツ・カテゴリを提供します。\n\nまた、公開中フォームの項目定義の配信（`GET /api/v1/forms/{key}`）と入力値の送信受付（`POST /api/v1/forms/{key}/submissions`）を提供します。送信データはテナント管理画面へ蓄積します。フォーム送信API（POST）は書き込み系のエンドポイント、フォーム配信API（GET）も送信速度チェック用の署名トークン（`speed_check_token`）を都度発行するため、いずれもキャッシュ対象外（`Cache-Control: no-store`）とし、スパム対策（ハニーポット・Cloudflare Turnstile・送信速度チェック・重複送信チェック・送信レートリミット）を標準提供します（各対策はフォーム設定でON/OFF可能。無効時はスキップ）。\n\n- 公開済みのコンテンツのみを返却します。\n- テナントの識別は、テナントごとに発行するAPIキー（`X-API-KEY`ヘッダ）で行います。APIキーはテナントあたり最大3件まで発行でき、**そのうち有効ないずれか1件**を渡せば認証されます（キーのローテーション用途）。\n- siteの特定は、APIキー（テナント特定）＋サイト識別子（`X-SITE-CODE`ヘッダ）で行います。\n- 管理APIは初期リリースでは提供しません（コンテンツ操作は管理画面経由に限定。将来の拡張候補）。\n- 多言語オプション（有料オプション・要件定義書 9.10）有効時は、コンテンツ取得系（投稿一覧・個別／FAQ一覧／固定コンテンツ／各カテゴリ一覧／フォーム定義／メンテナンス状態）で `locale` クエリにより言語別の公開版を返します。対応言語は `en`／`zh-Hans`／`zh-Hant`／`ko`／`vi`／`fr`／`de`（原文＝日本語 `ja`）。未指定は原文（日本語）、未設定・未対応の言語は `400`（`E001008`）。対象言語の公開版が無いコンテンツは原文を返し、`translation_available: false` を含めます。`locale` はキャッシュキーに含めます（`Accept-Language` では出し分けません）。\n\n## レートリミット\n\n- **テナント単位（プラン別）**の上限を超過した場合は `429 Too Many Requests`（`E001003`）を返却します。\n- **閲覧者IP単位**（全プラン共通 300 req/min）の上限を超過した場合は `403 Forbidden`（`E001004`）を返却します。\n- Businessプラン以上でアクセス元IP許可制御を有効にしている場合、許可外IPからのアクセスは `403 Forbidden`（`E001005`）を返却します。\n- レート状況は `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` および `Retry-After` ヘッダで返却します。これらのレスポンスには `Cache-Control: no-store` を付与します。\n- 初期リリース後の一定期間は遮断を行わず、超過の記録・通知のみを行います（実測後に遮断を有効化）。\n\n## CORS（ブラウザからの別オリジン直接アクセス）\n\n- 公開API（`/api/v1/*`）は、各Webサイトのブラウザからの**別オリジン直接アクセス（CORS）**に対応します（SPA／静的サイトからSDKで直接取得する用途）。\n- **許可オリジンは固定の列挙ではなく、各サイトに登録された公開ドメイン（`m_sites.url`・運営がサイト追加・編集画面〔SCR-012〕で設定）から自動導出**します。`url` を `scheme + host (+port)` に正規化した1オリジンを**完全一致**で許可します（パス・末尾スラッシュは無視。1サイト1オリジン。`url` にスキームが無い裸ホスト名〔例 `example.com`〕で登録されている場合も `https` を補って正規化するため、ホスト名のみの登録でも許可に反映されます）。サイトの公開ドメインを登録・変更すると、追加のデプロイ無しで許可オリジンへ反映されます。\n- プリフライト（`OPTIONS`）に対応し、`Access-Control-Allow-Origin` には登録済みオリジンのみを反射します。**Cookieは使用しません（`supports_credentials=false`）**。実リクエストの認証は従来どおり `X-API-KEY`＋`X-SITE-CODE` で行うため、オリジン許可は「どのブラウザ起点にレスポンスを晒すか」のみを制御し、未登録オリジン・無効キーではデータを取得できません。\n- レート状況ヘッダ（`X-RateLimit-*`／`Retry-After`）は `Access-Control-Expose-Headers` で公開し、ブラウザ（SDK）から参照できます。\n- 別オリジン直接アクセスでは **APIキーがブラウザに露出**します。キーの秘匿が必要な場合は、サイト側に同一オリジンのサーバー側中継（`cms-proxy.php`）を置く方式も選べます（同一オリジンのためCORS不要・キーはサーバー側のみに存在）。",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://cms-api.sample.net",
      "description": "公開API production server（ドメインは仮）"
    },
    {
      "url": "https://cms-api.dev.sample.net",
      "description": "公開API develop server（ドメインは仮）"
    }
  ],
  "tags": [
    {
      "name": "posts",
      "description": "投稿に関するエンドポイント"
    },
    {
      "name": "post-types",
      "description": "投稿の種別（post type）一覧に関するエンドポイント"
    },
    {
      "name": "preview",
      "description": "投稿プレビューに関するエンドポイント。下書き版をプレビュー用トークンで取得する"
    },
    {
      "name": "faqs",
      "description": "FAQに関するエンドポイント"
    },
    {
      "name": "contents",
      "description": "固定コンテンツに関するエンドポイント"
    },
    {
      "name": "categories",
      "description": "カテゴリ（投稿は種別スコープ）に関するエンドポイント"
    },
    {
      "name": "forms",
      "description": "フォームに関するエンドポイント"
    },
    {
      "name": "maintenance",
      "description": "メンテナンス状態に関するエンドポイント"
    },
    {
      "name": "changes",
      "description": "変更検知（増分SSG向け差分配信）に関するエンドポイント"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/api/v1/posts/{postType}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/PostType"
        }
      ],
      "get": {
        "tags": [
          "posts"
        ],
        "summary": "投稿一覧取得API",
        "operationId": "getPostList",
        "description": "指定した種別（`postType`）の公開済みの投稿一覧を返却するAPI。\n\n- 公開済み（`status` が公開、かつ `published_at` がリクエスト時刻以前）のもののみ返却する。\n- **上部固定（`pinned`）した投稿を先頭に、以降を並び順（`sort_order`）の昇順でソートして返却する**。固定が複数ある場合は、その中で並び順（`sort_order`）の昇順に並ぶ。カテゴリ絞り込み時も固定を先頭にする。ページネーションはこの並び順に対して行う。\n- `category`（カテゴリslug）による絞り込みが可能。\n- ページネーションに対応する。\n- 一覧では本文（`body`）は返却しない。本文は個別取得API（`GET /api/v1/posts/{postType}/{slug}`）で取得する。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/CategoryFilter"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "投稿一覧の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "投稿一覧（固定を先頭に並び順の昇順）",
                      "items": {
                        "$ref": "#/components/schemas/PostSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    }
                  },
                  "required": [
                    "items",
                    "meta"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "items": [
                        {
                          "slug": "new-office-opening",
                          "title": "新オフィス開設の投稿",
                          "published_at": "2026-07-01T10:00:00+09:00",
                          "pinned": true,
                          "category": {
                            "name": "プレスリリース",
                            "slug": "press-release"
                          },
                          "tags": [
                            "オフィス",
                            "移転"
                          ],
                          "eyecatch_image": {
                            "url": "https://cms-images.sample.net/images/1/abc123.webp",
                            "thumbnail_url": "https://cms-images.sample.net/images/1/abc123_thumb.webp",
                            "width": 1200,
                            "height": 630
                          }
                        }
                      ],
                      "meta": {
                        "current_page": 1,
                        "last_page": 5,
                        "per_page": 20,
                        "total": 100
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/posts/{postType}/categories": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/PostType"
        }
      ],
      "get": {
        "tags": [
          "categories"
        ],
        "summary": "投稿カテゴリ一覧取得API",
        "operationId": "getPostCategories",
        "description": "指定した種別（`postType`）に属するカテゴリ一覧を返却するAPI。\n\n- 公開済み（`is_public` が true・未削除）のカテゴリのみ返却する。\n- 表示順（`sort_order`）の昇順でソートして返却する。\n- 多言語オプション有効時は `locale` でカテゴリ名を言語別に返す（slug は言語共通・9.10.1）。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "投稿カテゴリ一覧の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "カテゴリ一覧（表示順の昇順）",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "items": [
                        {
                          "name": "プレスリリース",
                          "slug": "press-release"
                        },
                        {
                          "name": "イベント",
                          "slug": "event"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/posts/{postType}/{slug}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/PostType"
        },
        {
          "name": "slug",
          "in": "path",
          "required": true,
          "description": "投稿の識別子（site＋種別内で一意）。\n※`categories` はカテゴリ一覧APIのパスとして予約されているため、slugには使用できない。",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "posts"
        ],
        "summary": "投稿個別取得API",
        "operationId": "getPostBySlug",
        "description": "slugを指定して公開済みの投稿を1件返却するAPI。\n\n- 公開済み（`status` が公開、かつ `published_at` がリクエスト時刻以前）のもののみ返却する。\n- 未公開・下書き・存在しないslugの場合は404を返却する。\n- 多言語オプション有効時は `locale` で言語別の公開版（タイトル・本文・メタ等）を返す。対象言語の公開版が無い場合は原文を返し `translation_available: false` を含める（9.10.1）。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "投稿の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostDetail"
                },
                "examples": {
                  "default": {
                    "value": {
                      "slug": "new-office-opening",
                      "title": "新オフィス開設の投稿",
                      "published_at": "2026-07-01T10:00:00+09:00",
                      "pinned": false,
                      "category": {
                        "name": "プレスリリース",
                        "slug": "press-release"
                      },
                      "tags": [
                        "オフィス",
                        "移転"
                      ],
                      "eyecatch_image": {
                        "url": "https://cms-images.sample.net/images/1/abc123.webp",
                        "thumbnail_url": "https://cms-images.sample.net/images/1/abc123_thumb.webp",
                        "width": 1200,
                        "height": 630
                      },
                      "body": {
                        "type": "doc",
                        "content": [
                          {
                            "type": "paragraph",
                            "content": [
                              {
                                "type": "text",
                                "text": "このたび新オフィスを開設いたしました。"
                              }
                            ]
                          }
                        ]
                      },
                      "meta": {
                        "meta_title": "新オフィス開設のお知らせ｜DEMO株式会社",
                        "meta_description": "DEMO株式会社は2026年7月に新オフィスを開設します。",
                        "og_image_url": "https://cms-images.sample.net/images/1/og123.webp"
                      },
                      "related_urls": [
                        {
                          "type": "post",
                          "label": "新型精密プレス機「MP-850」を発売しました",
                          "url": "https://www.example.co.jp/news/mp-850-release",
                          "post": {
                            "post_type": {
                              "name": "お知らせ",
                              "slug": "news"
                            },
                            "slug": "mp-850-release",
                            "title": "新型精密プレス機「MP-850」を発売しました",
                            "published_at": "2026-06-20T10:00:00+09:00",
                            "eyecatch_image": {
                              "url": "https://cms-images.sample.net/images/1/mp850.webp",
                              "thumbnail_url": "https://cms-images.sample.net/images/1/mp850_thumb.webp",
                              "width": 1200,
                              "height": 630
                            },
                            "meta": {
                              "meta_title": "新型精密プレス機「MP-850」を発売",
                              "meta_description": "高精度・高速の新型プレス機 MP-850 を発売しました。",
                              "og_image_url": "https://cms-images.sample.net/images/1/mp850.webp"
                            }
                          }
                        },
                        {
                          "type": "external",
                          "label": "製品ページ：精密プレス機 MP-850",
                          "url": "https://www.example.co.jp/products/mp-850",
                          "post": null
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/posts/{postType}/{slug}/preview": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/PostType"
        },
        {
          "name": "slug",
          "in": "path",
          "required": true,
          "description": "投稿の識別子（site＋種別内で一意）。下書き版で識別する。",
          "schema": {
            "type": "string"
          }
        },
        {
          "$ref": "#/components/parameters/PreviewToken"
        }
      ],
      "get": {
        "tags": [
          "preview"
        ],
        "summary": "投稿プレビュー取得API",
        "operationId": "getPostPreviewBySlug",
        "description": "プレビュー用トークンを用いて、投稿の**下書き版**を1件返却するAPI。\n\nテナント管理画面の投稿編集画面のプレビューボタンで発行したトークンを用い、公開前・編集中の内容を実際のサイト表示で確認するために用いる。\n\n- 通常の公開投稿取得（`GET /api/v1/posts/{postType}/{slug}`）が常に公開済みのみを返すのに対し、本APIは**下書き版**を返す（未公開の新規／公開済み投稿の未公開変更／変更のない公開済み投稿、のいずれも下書き版を描画する）。\n- **認証**：プレビュー用トークンを必須とする。`X-PREVIEW-TOKEN` ヘッダ（**推奨**。ログへのトークン露出を避ける）または `?token=` クエリで受け付ける。既存公開APIの認証（`X-API-KEY`＋`X-SITE-CODE`）とは別レイヤの認証で、両方を満たす必要がある。\n- **404 に統一**：トークンが不正・期限切れ（既定24時間）・失効済み（投稿／サイト削除・管理者による明示失効）・対象不一致（トークンのスコープと `slug`／サイトが一致しない）の場合は、いずれも **404** を返す（存在秘匿のため 401/403 とせず、公開APIの未公開＝404方針と一貫）。\n- **レスポンス形状は公開API（`GET /api/v1/posts/{postType}/{slug}`）と完全に同一**（`PostDetail`）。公開／下書きの区別を外部に漏らさないため、`status` 等の内部状態フィールドは含めない。フロントエンドは公開ページと同一の表示ロジックを再利用できる。\n- **キャッシュしない**：下書き変更後に即座に最新内容を確認できるよう、公開APIのレスポンスキャッシュ経路（CloudFront／共有キャッシュ）を経由せず、成功・エラーとも `Cache-Control: no-store` を付与する。\n- プレビューページ側は検索エンジンにインデックスされないよう `noindex,nofollow` を設定する（フロントエンド責務）。",
        "security": [
          {
            "ApiKeyAuth": [],
            "PreviewTokenAuth": []
          },
          {
            "ApiKeyAuth": [],
            "PreviewTokenQueryAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "投稿下書き版の取得成功。レスポンス形状は公開API（投稿個別取得）と同一。\nレスポンスには `Cache-Control: no-store` を付与し、CDN・ブラウザともキャッシュしない。",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlNoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostDetail"
                },
                "examples": {
                  "default": {
                    "value": {
                      "slug": "new-office-opening",
                      "title": "新オフィス開設の投稿（編集中）",
                      "published_at": "2026-07-01T10:00:00+09:00",
                      "category": {
                        "name": "プレスリリース",
                        "slug": "press-release"
                      },
                      "tags": [
                        "オフィス",
                        "移転"
                      ],
                      "eyecatch_image": {
                        "url": "https://cms-images.sample.net/images/1/abc123.webp",
                        "thumbnail_url": "https://cms-images.sample.net/images/1/abc123_thumb.webp",
                        "width": 1200,
                        "height": 630
                      },
                      "body": {
                        "type": "doc",
                        "content": [
                          {
                            "type": "paragraph",
                            "content": [
                              {
                                "type": "text",
                                "text": "このたび新オフィスを開設いたします（プレビュー・下書き版）。"
                              }
                            ]
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundPreview"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/posts/{postType}/{slug}/unlock": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/PostType"
        },
        {
          "name": "slug",
          "in": "path",
          "required": true,
          "description": "投稿の識別子（site＋種別内で一意）。",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "posts"
        ],
        "summary": "投稿パスワード照合API",
        "operationId": "unlockPostBySlug",
        "description": "パスワード保護された投稿（`is_password_protected=true`）の本文を、閲覧パスワードの照合と引き換えに取得するAPI（SCR-104）。\n\n- 個別取得（`GET /api/v1/posts/{postType}/{slug}`）は保護中の投稿の本文（`body`）を返さない。本APIに正しい閲覧パスワードを送ると、本文入りの `PostDetail`（`GET` と同一形状）を返す。\n- **多言語オプション**：有効時は `locale` で言語別の公開版（本文・タイトル・メタ等）を返す（個別取得 `GET` と同一。対象言語の公開版が無い場合は原文を返し `translation_available: false` を含める・9.10.1）。`locale` を指定しないと本文が原文（日本語）のまま返るため、保護中の投稿では個別取得で得たタイトルと言語が食い違う点に注意する。\n- **照合失敗**：パスワードが一致しない場合は 401（`E001006`）。投稿の存在自体はタイトル等が一覧で既に公開されているため秘匿しない。\n- **総当たり対策**：site＋種別＋slug＋閲覧者IP単位で試行回数を制限し、超過時は 429（`E001007`・`Retry-After` を付与）。閲覧者IPの判定は公開API共通（`CloudFront-Viewer-Address`）に従う。\n- **未公開・存在しないslug**：存在秘匿のため 404。\n- **保護なし投稿**：本文が元々公開のため、パスワードに関わらず本文入りで返す。\n- **キャッシュしない**：保護本文を共有キャッシュ（CloudFront／レスポンスキャッシュ）に載せないため、成功・エラーとも `Cache-Control: no-store` を付与する。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "閲覧パスワード（管理側で設定した値）",
                    "example": "minato2026"
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "照合成功（または保護なし投稿）。本文入りの投稿を返す。レスポンス形状は投稿個別取得（`PostDetail`）と同一。\nレスポンスには `Cache-Control: no-store` を付与し、CDN・ブラウザともキャッシュしない。",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlNoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/post-types": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        }
      ],
      "get": {
        "tags": [
          "post-types"
        ],
        "summary": "種別一覧取得API",
        "operationId": "getPostTypes",
        "description": "siteの投稿の種別一覧を返却するAPI。\n\n- 公開サイトが種別ごとのページ（`/api/v1/posts/{postType}`）を出し分けるための discovery に用いる。\n- 表示順（`sort_order`）の昇順でソートして返却する。\n- 既定種別「お知らせ」（slug `news`）を含む。",
        "responses": {
          "200": {
            "description": "種別一覧の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "種別一覧（表示順の昇順）",
                      "items": {
                        "$ref": "#/components/schemas/PostTypeSummary"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "items": [
                        {
                          "name": "お知らせ",
                          "slug": "news",
                          "is_default": true
                        },
                        {
                          "name": "導入事例",
                          "slug": "case",
                          "is_default": false
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/faqs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        }
      ],
      "get": {
        "tags": [
          "faqs"
        ],
        "summary": "FAQ一覧取得API",
        "operationId": "getFaqList",
        "description": "公開済みのFAQ一覧を返却するAPI。\n\n- 公開済み（`status` が公開、かつ `published_at` がリクエスト時刻以前）のもののみ返却する。\n- 並び順（`sort_order`）の昇順でソートして返却する。\n- `category`（カテゴリslug）による絞り込みが可能。\n- FAQは件数が限定的なため、ページネーションは行わず全件返却する。\n- 多言語オプション有効時は `locale` で質問・回答・カテゴリ名を言語別に返す（9.10.1）。",
        "parameters": [
          {
            "$ref": "#/components/parameters/CategoryFilter"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "FAQ一覧の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "FAQ一覧（並び順の昇順）",
                      "items": {
                        "$ref": "#/components/schemas/Faq"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "items": [
                        {
                          "id": 1,
                          "question": "営業時間を教えてください。",
                          "answer": {
                            "type": "doc",
                            "content": [
                              {
                                "type": "paragraph",
                                "content": [
                                  {
                                    "type": "text",
                                    "text": "平日9時から18時まで営業しています。"
                                  }
                                ]
                              }
                            ]
                          },
                          "category": {
                            "name": "ご利用について",
                            "slug": "usage"
                          }
                        },
                        {
                          "id": 2,
                          "question": "駐車場はありますか。",
                          "answer": {
                            "type": "doc",
                            "content": [
                              {
                                "type": "paragraph",
                                "content": [
                                  {
                                    "type": "text",
                                    "text": "建物地下に共用駐車場がございます。"
                                  }
                                ]
                              }
                            ]
                          },
                          "category": {
                            "name": "アクセス",
                            "slug": "access"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/faqs/categories": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        }
      ],
      "get": {
        "tags": [
          "categories"
        ],
        "summary": "FAQカテゴリ一覧取得API",
        "operationId": "getFaqCategories",
        "description": "FAQ用のカテゴリ一覧を返却するAPI。\n\n- 公開済み（`is_public` が true・未削除）のカテゴリのみ返却する。\n- 表示順（`sort_order`）の昇順でソートして返却する。\n- 多言語オプション有効時は `locale` でカテゴリ名を言語別に返す（slug は言語共通・9.10.1）。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "FAQカテゴリ一覧の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "description": "カテゴリ一覧（表示順の昇順）",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "items": [
                        {
                          "name": "ご利用について",
                          "slug": "usage"
                        },
                        {
                          "name": "アクセス",
                          "slug": "access"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/contents/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "固定コンテンツの識別子（site内で一意）",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "contents"
        ],
        "summary": "固定コンテンツ取得API",
        "operationId": "getContentByKey",
        "description": "keyを指定して公開済みの固定コンテンツを1件返却するAPI。\n\n- 公開済み（`status` が公開、かつ `published_at` がリクエスト時刻以前）のもののみ返却する。\n- 非公開・未公開・存在しないkeyの場合は404を返却する。\n- 本文はTiptap（ProseMirrorドキュメント）のJSONで返却する。\n- 多言語オプション有効時は `locale` で本文を言語別に返す。対象言語の公開版が無い場合は原文を返し `translation_available: false` を含める（9.10.1）。",
        "parameters": [
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "固定コンテンツの取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Content"
                },
                "examples": {
                  "default": {
                    "value": {
                      "key": "company-profile",
                      "name": "会社概要",
                      "content_type": "rich_text",
                      "body": {
                        "type": "doc",
                        "content": [
                          {
                            "type": "paragraph",
                            "content": [
                              {
                                "type": "text",
                                "text": "株式会社サンプルの会社概要です。"
                              }
                            ]
                          }
                        ]
                      },
                      "updated_at": "2026-07-01T10:00:00+09:00"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/forms/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "フォームの識別子（site内で一意）",
          "schema": {
            "type": "string"
          }
        },
        {
          "$ref": "#/components/parameters/Locale"
        }
      ],
      "get": {
        "tags": [
          "forms"
        ],
        "summary": "フォーム項目定義取得API",
        "operationId": "getFormByKey",
        "description": "keyを指定して公開中フォームの項目定義（配信）を返却するAPI。\n\n- 公開中のフォームのみ返却する。\n- 非公開・存在しないkeyの場合は404を返却する。\n- 多言語オプション有効時は `locale` でフォーム定義のテキスト（項目ラベル等）を言語別に返す。フォーム定義は**常に自動翻訳**（手動編集なし）で、対象言語の訳が無い場合は原文を返し `translation_available: false` を含める（9.10.1／9.10.4）。`name`（フォーム名）・選択肢ラベルも翻訳対象に含める。`name`（項目のキー）・`type` 等の識別子は言語共通（翻訳しない）。\n- 本APIは送信速度チェック用の署名トークン（`speed_check_token`）を**リクエストごとに発行**するため、配信レスポンスは**キャッシュ対象外**（`Cache-Control: no-store`）とする。`fields` 自体は静的だが、トークンの発行時刻（＝ページ表示時刻）を各アクセスで正しく反映する必要があるため、レスポンス全体をキャッシュしない（キャッシュすると全利用者が同一トークンを共有し、送信速度チェックがキャッシュ経過時間を測る形になり機能しない）。\n- フロントがスパム対策ウィジェットを描画・トークン付与できるよう、サイト側描画に必要な範囲のみ（`turnstile`・`speed_check_token`）を返却する。ハニーポット・レートリミット・重複送信チェックの内部詳細は返却しない。\n  - `turnstile`: Turnstile有効時のみ `site_key` を返す（`secret_key` は絶対に返却しない）。\n  - `speed_check_token`: 送信速度チェック有効時に発行する、発行日時＋nonceをサーバ署名した改ざん検知可能なトークン。送信（POST）時に経過秒判定・改ざん検知に用いる。トークンの発行時刻を各アクセスで正しく反映するため、上記のとおり配信レスポンスはキャッシュしない（`Cache-Control: no-store`）。",
        "responses": {
          "200": {
            "description": "フォーム項目定義の取得成功。\nリクエストごとに `speed_check_token` を発行するため `Cache-Control: no-store` を付与し、CDN・ブラウザともキャッシュしない。",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlNoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Form"
                },
                "examples": {
                  "default": {
                    "value": {
                      "key": "contact",
                      "name": "お問い合わせフォーム",
                      "translation_available": true,
                      "turnstile": {
                        "enabled": true,
                        "site_key": "0x4AAAAAAABkMYinukE8nzYS"
                      },
                      "speed_check_token": "eyJpYXQiOjE3NTk1NzYwMDAsIm5vbmNlIjoiYTFiMmMzZDQifQ.c2lnbmF0dXJl",
                      "fields": [
                        {
                          "type": "text",
                          "name": "name",
                          "label": "お名前",
                          "required": true
                        },
                        {
                          "type": "email",
                          "name": "email",
                          "label": "メールアドレス",
                          "required": true
                        },
                        {
                          "type": "tel",
                          "name": "tel",
                          "label": "電話番号",
                          "required": false
                        },
                        {
                          "type": "select",
                          "name": "inquiry_type",
                          "label": "お問い合わせ種別",
                          "required": true,
                          "options": [
                            {
                              "value": "product",
                              "label": "製品について"
                            },
                            {
                              "value": "support",
                              "label": "サポートについて"
                            }
                          ]
                        },
                        {
                          "type": "textarea",
                          "name": "message",
                          "label": "お問い合わせ内容",
                          "required": true
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/forms/{key}/submissions": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "name": "key",
          "in": "path",
          "required": true,
          "description": "フォームの識別子（site内で一意）",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "forms"
        ],
        "summary": "フォーム送信API",
        "operationId": "createFormSubmission",
        "description": "公開中フォームへの送信を受け付け、送信データをテナント管理画面へ蓄積するAPI。\n\n- 送信（POST）はキャッシュ対象外。すべてのレスポンス（成功・エラーとも）に `Cache-Control: no-store` を付与する。\n- リクエストボディは、各フォーム項目の `name` をキー・入力値を値とするマップに、スパム対策用の項目（`_hp_name`・`_hp_website`・`speed_check_token`・`turnstile_token`）を加えたもの。\n- スパム対策（ハニーポット・Cloudflare Turnstile・送信速度チェック・重複送信チェック・レートリミット）を標準提供する。各対策はフォーム設定でON/OFF可能で、無効な対策の段階はスキップする。\n\n処理順（上から順に評価し、いずれかで拒否した時点で以降は実行しない）：\n\n1. フォーム存在確認：存在しないkeyは404（`E000007`）。\n2. 公開状態確認：非公開のフォームは404（`E000007`）。\n3. リクエストサイズ確認：ペイロードが上限を超過した場合は400（`E000002`）で拒否する。\n4. 入力バリデーション：項目定義（`fields`）に基づく必須・型・メール形式・選択肢等の検証。失敗は422（`E000001`）。\n5. レートリミット〔有効時〕：閲覧者IP 3回/分・フォーム 10回/10分。超過は429（`E001003`）を返し、`Retry-After` ヘッダで再試行までの待機秒数を返す。\n6. Turnstile検証〔有効時〕：`turnstile_token` をCloudflare Siteverifyでサーバ検証する。検証失敗は400（`E000002`）。\n7. ハニーポット検査〔有効時〕：`_hp_name` / `_hp_website` が空でない場合はボットとみなし422（`E000001`）。\n8. 送信速度チェック〔有効時〕：`speed_check_token`（サーバ署名）の改ざん検知と発行からの経過秒を判定し、2秒未満はスパムとみなし422（`E000001`）。\n9. 重複送信チェック〔有効時〕：formId＋email＋本文のハッシュで直近の重複送信を検出し、重複は422（`E000001`）。\n10. DB保存：送信データをテナント管理画面へ蓄積する。\n11. メール送信ジョブ登録：自動返信メール（送信者宛）・受信通知メール（管理者宛）を非同期ジョブで送信する（フォーム設定でON/OFF）。メール送信の失敗はAPIの成否に影響させない（APIは失敗させない）。\n12. 正常レスポンス：201（受付結果を返却）。\n\n- 上記に加え、閲覧者IP単位の共通レート上限（全プラン 300 req/min）を超過した場合は403（`E001004`）を返却する。",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FormSubmissionRequest"
              },
              "examples": {
                "default": {
                  "value": {
                    "name": "山田太郎",
                    "email": "taro@example.com",
                    "tel": "03-1234-5678",
                    "inquiry_type": "product",
                    "message": "製品について問い合わせたいことがあります。",
                    "_hp_name": "",
                    "_hp_website": "",
                    "speed_check_token": "eyJpYXQiOjE3NTk1NzYwMDAsIm5vbmNlIjoiYTFiMmMzZDQifQ.c2lnbmF0dXJl",
                    "turnstile_token": "0.abc123DEF456...xyz"
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/FormSubmissionRequest"
                  }
                ],
                "description": "ファイル項目（`type=file`）を含むフォームは `multipart/form-data` で送信する。\n各ファイル項目はその項目名（`name`）のバイナリパートとして送り、他の項目・スパム対策トークン（`_hp_*`／`speed_check_token`／`turnstile_token`）は同一フォームのフィールドとして送る。\nアップロードされたファイルはS3に保存され、送信データ（`payload`）にはファイル参照（`{name, s3_key, size, mime}`）が格納される。1ファイルの上限は10MB、許可形式は画像（png/jpeg）・PDF・Word・Excel・テキスト。"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "フォーム送信の受付成功。送信データをテナント管理画面へ蓄積する。\nレスポンスには `Cache-Control: no-store` を付与する。",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlNoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormSubmissionResult"
                },
                "examples": {
                  "default": {
                    "value": {
                      "id": 12345,
                      "submitted_at": "2026-07-30T10:00:00+09:00"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/maintenance": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        },
        {
          "$ref": "#/components/parameters/Locale"
        }
      ],
      "get": {
        "tags": [
          "maintenance"
        ],
        "summary": "メンテナンス状態取得API",
        "operationId": "getMaintenance",
        "description": "siteのメンテナンス状態を返却するAPI。\n\n- メンテナンス中の場合は `maintenance: true` と、見出し（`title`）・案内メッセージ（`message`）・再開予定日時（`resumes_at`）を返却する。\n- メンテナンス中でない場合は `maintenance: false` のみを返却する。\n- 公開サイトはこの値を見てメンテナンス画面の表示要否を判断する。\n- 投稿・FAQ・固定コンテンツ等の他コンテンツAPIとは独立しており、site単位の `uses_*` 設定（利用するコンテンツ設定）による404ゲートの対象外として常に応答する。\n- 多言語オプション有効時は `locale` で `title`／`message` を言語別に返す。メンテナンス状態は**常に自動翻訳**（手動編集なし）で、対象言語の訳が無い場合は原文を返し `translation_available: false` を含める（`resumes_at` は翻訳対象外・9.10.1／9.10.4）。\n- 状態を即時反映するため、キャッシュTTLは短くする（またはキャッシュ対象外とする）。",
        "responses": {
          "200": {
            "description": "メンテナンス状態の取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Maintenance"
                },
                "examples": {
                  "inactive": {
                    "summary": "メンテナンス中でない場合",
                    "value": {
                      "maintenance": false
                    }
                  },
                  "active": {
                    "summary": "メンテナンス中の場合",
                    "value": {
                      "maintenance": true,
                      "title": "ただいまメンテナンス中です",
                      "message": "サービス改善のためメンテナンスを実施しています。ご不便をおかけし申し訳ございません。",
                      "resumes_at": "2026-08-15T06:00:00+09:00",
                      "translation_available": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidLocale"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/changes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        }
      ],
      "get": {
        "tags": [
          "changes"
        ],
        "summary": "変更検知API（増分SSG向け差分配信）",
        "operationId": "getChanges",
        "description": "サイト単位の最終ビルド日時（`m_sites.last_built_at`）以降に `updated_at` が更新された公開コンテンツの差分一覧を返すAPI。大量コンテンツサイトで、前回ビルド以降に変更があったページのみを再生成する増分SSGに用いる。\n\n- `last_built_at` が未設定（NULL）の場合は初回ビルドとみなし、現在公開中の全コンテンツを `upsert` で列挙し `full_resync: true` を返す。\n- **カテゴリ・タグ名の変更、およびサイト設定画面（SCR-120）からの手動ビルドは全体ビルド**とし `full_resync: true` を返す（カテゴリ名・タグ名は多数の投稿記事に波及するため、個別差分ではなく全体再生成とする）。サーバーは該当契機で `m_sites.full_rebuild_requested_at` を更新し、`full_rebuild_requested_at > last_built_at` の間 `full_resync: true` とする（完了通知で `last_built_at` が前進すると自然に解消）。\n- フォーム定義の変更は局所的（特定ページに埋め込み・多くは実行時に `GET /api/v1/forms/{key}` で取得）なため全体ビルドにせず、`type=form` の差分エントリ（`action=upsert`）として返す。\n- 各レコードの `updated_at` を `last_built_at` と比較して差分を判定する（論理削除分も含めて走査）。\n- `action` は、対象レコードが**現在公開中なら `upsert`**（＝再生成。編集・公開・予約go-live・並べ替え・非公開解除を包含）、**非公開・論理削除・未公開なら `delete`**（＝SSGから該当ページを除去。元々無ければ無視）に二分する。\n- 予約投稿の公開（go-live）は、行の内容が変わらず `updated_at` が据え置かれるため、BAT-001（公開日時反映処理）が go-live 到来時に該当投稿の `updated_at` を更新することで差分に乗せる。\n- このAPIは watermark（`last_built_at`）を**進めない**（冪等）。レスポンスのサーバー時刻 `as_of` を、ビルド成功後に `POST /api/v1/changes/complete` の `built_as_of` として返すことで初めて前進する。これを呼ぶまでは同じ差分を何度でも返す。\n- 認証は公開APIと同じ `X-API-KEY`＋`X-SITE-CODE`。多言語（`locale`）は取らない（言語別の差分は原文の `updated_at` に紐づく）。",
        "responses": {
          "200": {
            "description": "差分フィードの取得成功。",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangeFeed"
                },
                "examples": {
                  "default": {
                    "summary": "通常の増分差分",
                    "value": {
                      "as_of": "2026-10-14T12:34:56+09:00",
                      "full_resync": false,
                      "changes": [
                        {
                          "type": "post",
                          "post_type": "news",
                          "key": "summer-campaign",
                          "action": "upsert",
                          "updated_at": "2026-10-14T10:00:00+09:00"
                        },
                        {
                          "type": "post",
                          "post_type": "news",
                          "key": "old-notice",
                          "action": "delete",
                          "updated_at": "2026-10-14T11:20:00+09:00"
                        },
                        {
                          "type": "category",
                          "post_type": null,
                          "key": "info",
                          "action": "upsert",
                          "updated_at": "2026-10-14T09:15:00+09:00"
                        }
                      ]
                    }
                  },
                  "full_resync": {
                    "summary": "初回（last_built_at 未設定）＝全件同期",
                    "value": {
                      "as_of": "2026-10-14T12:34:56+09:00",
                      "full_resync": true,
                      "changes": [
                        {
                          "type": "post",
                          "post_type": "news",
                          "key": "summer-campaign",
                          "action": "upsert",
                          "updated_at": "2026-10-14T10:00:00+09:00"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/v1/changes/complete": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SiteCode"
        }
      ],
      "post": {
        "tags": [
          "changes"
        ],
        "summary": "変更検知API・ビルド完了通知",
        "operationId": "completeChanges",
        "description": "増分ビルドが成功した後に呼び出し、サイトの最終ビルド日時（`m_sites.last_built_at`）を前進させるAPI。直前の `GET /api/v1/changes` が返した `as_of` を `built_as_of` として送る。\n\n- `last_built_at` は `built_as_of` が現在値以上のときのみ前進させる（巻き戻さない）。\n- 公開APIは閲覧専用（`api_public` ロールは `m_sites` への書き込み権限を持たない）ため、実際の `m_sites.last_built_at` 更新は**owner接続のキュージョブ経由**で行い、本APIは受理（202相当）のみ返す。\n- このAPIを呼ぶまでは `GET /api/v1/changes` は同じ差分を何度でも返す（ビルド失敗時は呼ばずに再試行すれば差分を取りこぼさない）。\n- full resync（全件再同期）をやり直したい場合は、運営側で `last_built_at` を NULL にリセットする（本APIでは行わない）。\n- 認証は公開APIと同じ `X-API-KEY`＋`X-SITE-CODE`。",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "built_as_of": {
                    "type": "string",
                    "format": "date-time",
                    "description": "直前の `GET /api/v1/changes` が返した `as_of`。この時刻まで増分ビルドが成功したことを表す。"
                  }
                },
                "required": [
                  "built_as_of"
                ]
              },
              "examples": {
                "default": {
                  "value": {
                    "built_as_of": "2026-10-14T12:34:56+09:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "完了通知を受理。最終ビルド日時の前進は owner接続のキュージョブ経由で非同期に反映される。",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accepted": {
                      "type": "boolean",
                      "description": "完了通知を受理したか。"
                    },
                    "built_as_of": {
                      "type": "string",
                      "format": "date-time",
                      "description": "受理した `built_as_of`（この時刻へ `last_built_at` を前進させる）。"
                    }
                  },
                  "required": [
                    "accepted",
                    "built_as_of"
                  ]
                },
                "examples": {
                  "default": {
                    "value": {
                      "accepted": true,
                      "built_as_of": "2026-10-14T12:34:56+09:00"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-KEY",
        "description": "テナントごとに発行するAPIキー。全エンドポイントで必須。\nテナントあたり最大3件・有効なキーを最低1件持ち、**有効ないずれか1件**を渡せば認証される（ローテーション用途）。\nキーはテナント設定画面（管理者ロール）またはShareDan運営管理画面で発行・再発行・有効/無効・削除でき、生値は発行・再発行時に一度だけ表示される（以降はプレフィックスのマスク表示）。"
      },
      "PreviewTokenAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-PREVIEW-TOKEN",
        "description": "投稿プレビュー取得API（`GET /api/v1/posts/{postType}/{slug}/preview`）専用のプレビュー用トークン。\nテナント管理画面の投稿編集画面で発行する。`X-API-KEY` とは別レイヤの認証で、プレビューAPIでのみ使用する。\nログへの露出を避けるため、クエリ（`?token=`）よりも本ヘッダでの受け渡しを推奨する。"
      },
      "PreviewTokenQueryAuth": {
        "type": "apiKey",
        "in": "query",
        "name": "token",
        "description": "`PreviewTokenAuth`（`X-PREVIEW-TOKEN` ヘッダ）と同一のプレビュー用トークンをクエリ文字列で渡す代替手段。\nヘッダ方式が利用できないフロントエンド向け。ログにトークンが残りうるためヘッダ方式を推奨する。"
      }
    },
    "parameters": {
      "SiteCode": {
        "name": "X-SITE-CODE",
        "in": "header",
        "required": true,
        "description": "サイト識別子。APIキーで特定したテナント配下のsiteを特定する。",
        "schema": {
          "type": "string"
        }
      },
      "PreviewToken": {
        "name": "token",
        "in": "query",
        "required": false,
        "description": "プレビュー用トークン（投稿プレビュー取得API）。`X-PREVIEW-TOKEN` ヘッダで渡す場合は省略する。\nヘッダ・クエリのいずれか一方でトークンを渡す（ログ露出を避けるためヘッダ方式を推奨）。",
        "schema": {
          "type": "string"
        }
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "description": "ページ番号",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "required": false,
        "description": "1ページあたりの件数",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "CategoryFilter": {
        "name": "category",
        "in": "query",
        "required": false,
        "description": "カテゴリslugによる絞り込み",
        "schema": {
          "type": "string"
        }
      },
      "PostType": {
        "name": "postType",
        "in": "path",
        "required": true,
        "description": "投稿の種別（post type）の識別子（slug）。既定種別は `news`。\n利用可能な種別は種別一覧API（`GET /api/v1/post-types`）で取得できる。存在しない種別は404を返す。",
        "schema": {
          "type": "string"
        }
      },
      "Locale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "description": "多言語オプション（有料オプション・要件定義書 9.10）。返す言語の言語コードを指定する。対応言語は `en`（英語）・`zh-Hans`（中国語簡体）・`zh-Hant`（中国語繁体）・`ko`（韓国語）・`vi`（ベトナム語）・`fr`（フランス語）・`de`（ドイツ語）。原文は日本語（`ja`）。\n- **未指定時は原文（日本語）**を返す。\n- 多言語オプションが無効、または運営管理画面で設定していない／未対応の言語を指定した場合は **400**（`E001008`）を返す。\n- 常に**各言語の公開版のみ**を返す。対象言語の公開版がまだ無いコンテンツは**原文を返し**、レスポンスに `translation_available: false` を含める。\n- 対応エンドポイント：投稿一覧・個別／FAQ一覧／固定コンテンツ／各カテゴリ一覧／**フォーム定義（`GET /api/v1/forms/{key}`）／メンテナンス状態（`GET /api/v1/maintenance`）**。\n- 翻訳対象はタイトル・本文・カテゴリ名・FAQの質問／回答・メタタグ／OGPのテキスト・**フォーム定義（項目ラベル等）・メンテナンス状態（`title`／`message`）**。slug・key 等の識別子は言語共通（翻訳しない）。カテゴリ名・フォーム定義・メンテナンスは常に自動翻訳。\n- `locale` はキャッシュキーに含める（`Accept-Language` では出し分けない）。",
        "schema": {
          "type": "string"
        },
        "example": "en"
      }
    },
    "schemas": {
      "PaginationMeta": {
        "type": "object",
        "description": "ページネーション情報",
        "properties": {
          "current_page": {
            "type": "integer",
            "description": "現在のページ番号"
          },
          "last_page": {
            "type": "integer",
            "description": "最終ページ番号"
          },
          "per_page": {
            "type": "integer",
            "description": "1ページあたりの件数"
          },
          "total": {
            "type": "integer",
            "description": "総件数"
          }
        },
        "required": [
          "current_page",
          "last_page",
          "per_page",
          "total"
        ]
      },
      "Category": {
        "type": "object",
        "description": "カテゴリ",
        "properties": {
          "name": {
            "type": "string",
            "description": "カテゴリ表示名"
          },
          "slug": {
            "type": "string",
            "description": "カテゴリ識別子（投稿=site＋種別内で一意／FAQ=site＋type内で一意）。言語共通（翻訳しない）"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` 指定時のみ返す翻訳有無フラグ。\ntrue＝指定言語のカテゴリ名の訳を返した／false＝訳が無く原文名を返した。`locale` 未指定時は省略。",
            "example": true
          }
        },
        "required": [
          "name",
          "slug"
        ]
      },
      "PostTypeSummary": {
        "type": "object",
        "description": "投稿の種別（公開・discovery用）",
        "properties": {
          "name": {
            "type": "string",
            "description": "種別の表示名",
            "example": "お知らせ"
          },
          "slug": {
            "type": "string",
            "description": "種別の識別子（`/api/v1/posts/{postType}` のパスに使用）",
            "example": "news"
          },
          "is_default": {
            "type": "boolean",
            "description": "既定種別か",
            "example": true
          }
        },
        "required": [
          "name",
          "slug",
          "is_default"
        ]
      },
      "EyecatchImage": {
        "type": "object",
        "nullable": true,
        "description": "アイキャッチ画像（未設定の場合はnull）",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "画像URL（1MB以内へ縮小・WebP変換済み）"
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "description": "サムネイル画像URL"
          },
          "width": {
            "type": "integer",
            "description": "幅（px）"
          },
          "height": {
            "type": "integer",
            "description": "高さ（px）"
          }
        },
        "required": [
          "url",
          "thumbnail_url"
        ]
      },
      "ProseMirrorBody": {
        "type": "object",
        "description": "本文（Tiptap／ProseMirrorドキュメントのJSON）。 画像ノード（`type: image`）の `attrs` には `src`（画像URL）・`alt`（代替テキスト）のほか、 管理側で設定した表示サイズ `width`・`height`（px。縦横比を維持。未設定時は省略）と キャプション `caption`（未設定時は空文字）が含まれる。公開サイトは `width`／`height` を `<img>` に付与することで画像読み込み時のレイアウトずれ（CLS）を防げる （SDK `renderProseMirror()` は自動で付与する）。",
        "example": {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "このたび新オフィスを開設いたしました。"
                }
              ]
            },
            {
              "type": "image",
              "attrs": {
                "src": "https://cms-images.sample.net/images/1/abc123.webp",
                "alt": "新オフィスの外観",
                "width": 640,
                "height": 360,
                "caption": "新オフィスの外観"
              }
            }
          ]
        }
      },
      "PostSummary": {
        "type": "object",
        "description": "投稿（一覧用・本文なし）",
        "properties": {
          "slug": {
            "type": "string",
            "description": "投稿の識別子（site内で一意）"
          },
          "title": {
            "type": "string",
            "description": "タイトル"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "description": "公開日時"
          },
          "pinned": {
            "type": "boolean",
            "description": "上部固定（ピン）。true の投稿は同一種別の一覧の先頭に表示される（`is_pinned`。公開サイトで「注目」等のバッジ表示に利用してよい）",
            "example": false
          },
          "is_password_protected": {
            "type": "boolean",
            "description": "閲覧パスワード保護（SCR-104）。true の投稿は本文が個別取得（`GET .../posts/{postType}/{slug}`）で返らず（`body` が null）、照合API（`POST .../unlock`）に正しいパスワードを送ったときだけ本文が返る。公開サイトで鍵アイコン等の表示に利用してよい。パスワード自体は一切返さない",
            "example": false
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "tags": {
            "type": "array",
            "description": "タグ名の一覧",
            "items": {
              "type": "string"
            }
          },
          "eyecatch_image": {
            "$ref": "#/components/schemas/EyecatchImage"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` を指定したときのみ返す翻訳有無フラグ。\ntrue＝指定言語の公開版が存在しその訳を返した／false＝公開版が無く原文を返した。\n`locale` 未指定（原文取得）時は省略する。",
            "example": true
          }
        },
        "required": [
          "slug",
          "title",
          "published_at",
          "pinned",
          "is_password_protected",
          "category",
          "tags"
        ]
      },
      "PostMeta": {
        "type": "object",
        "nullable": true,
        "description": "メタタグ・OGP設定（投稿個別取得のみ・任意）。管理側でStandardプラン以上のときに設定でき、\n公開ページのSEO/OGP出力に用いる。未設定の項目はnull（公開側でタイトル・本文抜粋等から既定値を補ってよい）。",
        "properties": {
          "meta_title": {
            "type": "string",
            "nullable": true,
            "description": "メタタイトル（`<title>` 相当）。未設定時は投稿タイトルを既定にしてよい"
          },
          "meta_description": {
            "type": "string",
            "nullable": true,
            "description": "メタディスクリプション（`<meta name=\"description\">`）"
          },
          "og_image_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "OGP画像URL（`og:image`）。設定された OGP 画像のURL。未設定時はアイキャッチ画像のURLで補う（両方未設定なら null）"
          }
        }
      },
      "PostRelatedUrl": {
        "type": "object",
        "description": "関連URL（管理側で設定した関連リンク）。`type` で外部URLと投稿参照を判別する。\n投稿参照（`type=post`）は同一サイト内の別の公開投稿を指し、`post` にアイキャッチ・メタタグ等の付属情報を含めて返す（リンクカード表示等に利用できる）。",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "external",
              "post"
            ],
            "description": "種別。`external`＝外部URL、`post`＝サイト内の別の公開投稿への参照"
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "表示テキスト（リンクテキスト）。`external` は管理側の入力値、`post` は参照先投稿のタイトル。未設定（null）の場合はURLをそのまま表示してよい"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "リンク先URL。`external` は入力値、`post` は参照先投稿の公開URL（`{サイトURL}/{種別slug}/{投稿slug}`）"
          },
          "post": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PostRelatedUrlPost"
              }
            ],
            "nullable": true,
            "description": "投稿参照の付属情報（`type=post` のときのみ返す）。`type=external` ではnull"
          }
        },
        "required": [
          "type",
          "url"
        ]
      },
      "PostRelatedUrlPost": {
        "type": "object",
        "description": "関連URLが参照する公開投稿の付属情報（`type=post` のとき返す）。アイキャッチ画像・メタタグ／OGPをリンクカード表示等に利用できる。\n参照先が非公開・予約公開前・削除済みになった場合は、その関連URL自体がレスポンスから除外される（付属情報だけがnullで返ることはない）。",
        "properties": {
          "post_type": {
            "type": "object",
            "description": "参照先投稿の種別",
            "properties": {
              "name": {
                "type": "string",
                "description": "種別の表示名",
                "example": "お知らせ"
              },
              "slug": {
                "type": "string",
                "description": "種別の識別子（`/api/v1/posts/{postType}` のパスに使用）",
                "example": "news"
              }
            },
            "required": [
              "name",
              "slug"
            ]
          },
          "slug": {
            "type": "string",
            "description": "参照先投稿の識別子（`GET /api/v1/posts/{postType}/{slug}` のキー）",
            "example": "mp-850-release"
          },
          "title": {
            "type": "string",
            "description": "参照先投稿のタイトル",
            "example": "新型精密プレス機「MP-850」を発売しました"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "description": "参照先投稿の公開日時"
          },
          "eyecatch_image": {
            "$ref": "#/components/schemas/EyecatchImage"
          },
          "meta": {
            "$ref": "#/components/schemas/PostMeta"
          }
        },
        "required": [
          "post_type",
          "slug",
          "title",
          "published_at"
        ]
      },
      "PostDetail": {
        "description": "投稿（個別取得用・本文あり）",
        "allOf": [
          {
            "$ref": "#/components/schemas/PostSummary"
          },
          {
            "type": "object",
            "properties": {
              "body": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ProseMirrorBody"
                  }
                ],
                "nullable": true,
                "description": "本文（ProseMirrorのJSON）。パスワード保護中（`is_password_protected=true`）でパスワード未照合のときは null（照合API `POST .../posts/{postType}/{slug}/unlock` で取得する）。保護なし・照合済みは本文を返す"
              },
              "meta": {
                "$ref": "#/components/schemas/PostMeta"
              },
              "related_urls": {
                "type": "array",
                "description": "関連URL（管理側で設定した順序のまま返す。未設定は空配列）。各要素は `type` で外部URL／投稿参照を判別する。\n投稿参照（`type=post`）で、参照先が非公開・予約公開前・削除済みになったものは配列から除外する（順序は詰める）。",
                "items": {
                  "$ref": "#/components/schemas/PostRelatedUrl"
                }
              }
            },
            "required": [
              "body",
              "related_urls"
            ]
          }
        ]
      },
      "Faq": {
        "type": "object",
        "description": "FAQ",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "FAQのID"
          },
          "question": {
            "type": "string",
            "description": "質問"
          },
          "answer": {
            "$ref": "#/components/schemas/ProseMirrorBody"
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` 指定時のみ返す翻訳有無フラグ。\ntrue＝指定言語の公開版の訳を返した／false＝公開版が無く原文を返した。`locale` 未指定時は省略。",
            "example": true
          }
        },
        "required": [
          "id",
          "question",
          "answer",
          "category"
        ]
      },
      "Content": {
        "type": "object",
        "description": "固定コンテンツ",
        "properties": {
          "key": {
            "type": "string",
            "description": "固定コンテンツの識別子（site内で一意）"
          },
          "name": {
            "type": "string",
            "description": "コンテンツ名"
          },
          "content_type": {
            "type": "string",
            "description": "コンテンツタイプ（エディタUI切替用）"
          },
          "body": {
            "$ref": "#/components/schemas/ProseMirrorBody"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "更新日時"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` 指定時のみ返す翻訳有無フラグ。\ntrue＝指定言語の公開版の訳を返した／false＝公開版が無く原文を返した。`locale` 未指定時は省略。",
            "example": true
          }
        },
        "required": [
          "key",
          "name",
          "content_type",
          "body",
          "updated_at"
        ]
      },
      "Maintenance": {
        "type": "object",
        "description": "サイトのメンテナンス状態",
        "properties": {
          "maintenance": {
            "type": "boolean",
            "description": "メンテナンス中かどうか"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "メンテナンス見出し（メンテナンス中のみ設定。`maintenance` が false の場合は省略）"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "メンテナンス案内メッセージ（メンテナンス中のみ設定。`maintenance` が false の場合は省略）"
          },
          "resumes_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "再開予定日時（任意。未設定の場合は null。翻訳対象外）"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` 指定時のみ返す翻訳有無フラグ。\ntrue＝指定言語の `title`／`message` の訳を返した／false＝訳が無く原文を返した。`locale` 未指定時・`maintenance` が false の場合は省略。",
            "example": true
          }
        },
        "required": [
          "maintenance"
        ]
      },
      "FormFieldOption": {
        "type": "object",
        "description": "フォーム項目の選択肢（type が select/radio/checkbox のときに使用）",
        "properties": {
          "value": {
            "type": "string",
            "description": "送信値（項目値として送信される識別子）"
          },
          "label": {
            "type": "string",
            "description": "表示名（ラベル）"
          }
        },
        "required": [
          "value",
          "label"
        ]
      },
      "FormField": {
        "type": "object",
        "description": "フォーム項目定義",
        "properties": {
          "type": {
            "type": "string",
            "description": "項目種別（`url` はURL（http／https形式）入力項目・送信時にURL形式を検証。`file` はファイル添付項目。送信は multipart/form-data、ファイルはS3に保存される）",
            "enum": [
              "text",
              "email",
              "tel",
              "url",
              "textarea",
              "radio",
              "checkbox",
              "select",
              "file"
            ]
          },
          "name": {
            "type": "string",
            "description": "項目識別子（送信時のキー。form内で一意）"
          },
          "label": {
            "type": "string",
            "description": "表示名（ラベル）"
          },
          "required": {
            "type": "boolean",
            "description": "必須項目かどうか"
          },
          "options": {
            "type": "array",
            "description": "選択肢（type が select/radio/checkbox のときに指定。それ以外の項目では省略）",
            "items": {
              "$ref": "#/components/schemas/FormFieldOption"
            }
          },
          "placeholder": {
            "type": "string",
            "nullable": true,
            "description": "入力欄のプレースホルダー（空欄時に薄く表示する案内文。type が text/email/tel/url/textarea のときに指定。それ以外はnull）"
          }
        },
        "required": [
          "type",
          "name",
          "label",
          "required"
        ]
      },
      "FormTurnstileConfig": {
        "type": "object",
        "description": "Cloudflare Turnstile設定（配信用）。\nサイト側でウィジェットを描画するために必要な範囲のみを公開する。`secret_key` は絶対に返却しない。",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Turnstileが有効かどうか"
          },
          "site_key": {
            "type": "string",
            "nullable": true,
            "description": "Turnstileのサイトキー（`enabled` が true のときのみ設定。無効時は null）"
          }
        },
        "required": [
          "enabled",
          "site_key"
        ]
      },
      "Form": {
        "type": "object",
        "description": "公開中フォームの項目定義（配信用）",
        "properties": {
          "key": {
            "type": "string",
            "description": "フォームの識別子（site内で一意）"
          },
          "name": {
            "type": "string",
            "description": "フォーム名（多言語オプション有効時は `locale` で言語別に返す・常に自動翻訳）"
          },
          "translation_available": {
            "type": "boolean",
            "description": "多言語オプション（9.10）で `locale` 指定時のみ返す翻訳有無フラグ。\ntrue＝指定言語のフォーム定義（項目ラベル等）の訳を返した／false＝訳が無く原文を返した。`locale` 未指定時は省略。",
            "example": true
          },
          "fields": {
            "type": "array",
            "description": "項目定義の配列（表示順。多言語オプション有効時は `label` 等を `locale` で言語別に返す・常に自動翻訳）",
            "items": {
              "$ref": "#/components/schemas/FormField"
            }
          },
          "turnstile": {
            "$ref": "#/components/schemas/FormTurnstileConfig"
          },
          "speed_check_token": {
            "type": "string",
            "nullable": true,
            "description": "送信速度チェック用の署名トークン。送信速度チェック有効時のみ発行し、無効時は null。\n発行日時＋nonceをサーバ署名した改ざん検知可能なトークンで、送信（POST）時に `speed_check_token` として送り返し、経過秒判定・改ざん検知に用いる。"
          }
        },
        "required": [
          "key",
          "name",
          "fields",
          "turnstile",
          "speed_check_token"
        ]
      },
      "FormSubmissionRequest": {
        "type": "object",
        "description": "フォーム送信リクエスト。\n各フォーム項目の `name` をキー・入力値を値とするマップに加え、スパム対策用の項目を含む。\n- 各項目の値は、項目種別に応じて文字列または文字列配列（checkbox等の複数選択時）で送信する。\n- `_hp_name` / `_hp_website`: ハニーポット項目。人間には非表示にし、空であることが正常。値が入っている場合はボットとみなし422で拒否する（ハニーポット有効時）。\n- `speed_check_token`: 配信（`GET /api/v1/forms/{key}`）で受け取ったサーバ署名トークンをそのまま送り返す。送信速度チェック有効時に必須で、改ざん検知と発行からの経過秒判定に用いる。\n- `turnstile_token`: Cloudflare Turnstileウィジェットが発行するトークン。Turnstile有効フォームでは必須。サーバ側でCloudflare Siteverifyにより検証する（検証失敗は400）。",
        "properties": {
          "_hp_name": {
            "type": "string",
            "nullable": true,
            "description": "ハニーポット項目（人間には非表示。空であることが正常）。"
          },
          "_hp_website": {
            "type": "string",
            "nullable": true,
            "description": "ハニーポット項目（人間には非表示。空であることが正常）。"
          },
          "speed_check_token": {
            "type": "string",
            "nullable": true,
            "description": "配信（GET）で受け取った送信速度チェック用の署名トークン。送信速度チェック有効時に必須。"
          },
          "turnstile_token": {
            "type": "string",
            "nullable": true,
            "description": "Cloudflare Turnstileトークン。Turnstile有効フォームでは必須。"
          }
        },
        "additionalProperties": true
      },
      "FormSubmissionResult": {
        "type": "object",
        "description": "フォーム送信の受付結果",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "受け付けた送信データのID（テナント管理画面で参照可能）"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "送信を受け付けた日時"
          }
        },
        "required": [
          "id",
          "submitted_at"
        ]
      },
      "ChangeFeed": {
        "type": "object",
        "description": "変更検知の差分フィード（増分SSG用）",
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "このレスポンスを生成したサーバー時刻。ビルド成功後に `POST /api/v1/changes/complete` の `built_as_of` として返す（次回ビルドの基準）。"
          },
          "full_resync": {
            "type": "boolean",
            "description": "全体ビルドのフラグ。`true` のときクライアントはサイト全体を再生成する（`changes` は現在公開中の全コンテンツを `upsert` で列挙）。次のいずれかで `true` になる：初回（`last_built_at` 未設定）／**カテゴリ・タグ名の変更**（カテゴリ名・タグ名は多数の投稿記事に波及するため全体ビルド）／**サイト設定画面（SCR-120）からの手動ビルド**。サーバーは該当契機で `m_sites.full_rebuild_requested_at` を更新し、`last_built_at IS NULL` または `full_rebuild_requested_at > last_built_at` の間 `true` を返す。なおフォーム定義の変更は局所的なため全体ビルドにせず、`type=form` の差分エントリ（`action=upsert`）で扱う。"
          },
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChangeItem"
            }
          }
        },
        "required": [
          "as_of",
          "full_resync",
          "changes"
        ]
      },
      "ChangeItem": {
        "type": "object",
        "description": "1コンテンツの変更",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "post",
              "faq",
              "content",
              "category",
              "form"
            ],
            "description": "コンテンツ種別。"
          },
          "post_type": {
            "type": "string",
            "nullable": true,
            "description": "`type=post` のときの種別slug（`/api/v1/posts/{postType}/{slug}` の `postType`）。それ以外は `null`。"
          },
          "key": {
            "type": "string",
            "description": "識別子。post=slug／content=key／faq=id（文字列化）／category=slug／form=key。"
          },
          "action": {
            "type": "string",
            "enum": [
              "upsert",
              "delete"
            ],
            "description": "`upsert`=現在公開中＝再生成／`delete`=非公開・削除・未公開＝SSGから除去（無ければ無視）。"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "このコンテンツの更新日時（`last_built_at` との比較基準。予約go-liveは BAT-001 が公開到来時に更新）。"
          }
        },
        "required": [
          "type",
          "key",
          "action",
          "updated_at"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "リクエストが不正（フォーム送信API）。以下のいずれか。\n- スパム対策（Cloudflare Turnstile）のトークン検証（Siteverify）に失敗した（`E000002`）。\n- リクエストサイズが上限を超過した（`E000002`）。\nレスポンスには `Cache-Control: no-store` を付与する。",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/CacheControlNoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "TurnstileVerificationFailed": {
                "summary": "Turnstile検証失敗",
                "value": {
                  "code": "E000002",
                  "message": "Bad Request",
                  "details": "スパム対策（Turnstile）の検証に失敗しました。"
                }
              },
              "RequestTooLarge": {
                "summary": "リクエストサイズ超過",
                "value": {
                  "code": "E000002",
                  "message": "Bad Request",
                  "details": "リクエストサイズが上限を超過しました。"
                }
              }
            }
          }
        }
      },
      "InvalidLocale": {
        "description": "多言語オプションの `locale` が不正（コンテンツ取得系・フォーム定義・メンテナンス状態・要件定義書 9.10.1）。以下のいずれか。\n- 多言語オプションが無効のサイトで `locale` を指定した（`E001008`）。\n- 運営管理画面で設定していない、または未対応の言語を指定した（`E001008`）。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "LocaleNotAvailable": {
                "summary": "利用できない言語",
                "value": {
                  "code": "E001008",
                  "message": "Bad Request",
                  "details": "指定された言語は利用できません。"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "APIキーまたはサイト識別子が不正。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "Unauthorized": {
                "value": {
                  "code": "E001001",
                  "message": "Unauthorized",
                  "details": "APIキーまたはサイト識別子が不正です。"
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "アクセスが拒否された。以下のいずれか。\n- テナントの利用が停止されている（`E001002`）。\n- 閲覧者IP単位のレート上限を超過した（`E001004`）。\n- 許可されていないIPアドレスからのアクセス（`E001005`。Businessプラン以上のIP許可制御）。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "TenantSuspended": {
                "summary": "テナント利用停止",
                "value": {
                  "code": "E001002",
                  "message": "Forbidden",
                  "details": "テナントの利用が停止されています。"
                }
              },
              "ViewerIpRateLimited": {
                "summary": "閲覧者IPのレート超過",
                "value": {
                  "code": "E001004",
                  "message": "Forbidden",
                  "details": "アクセス元IPのリクエスト上限を超過しました。"
                }
              },
              "IpNotAllowed": {
                "summary": "許可外IP",
                "value": {
                  "code": "E001005",
                  "message": "Forbidden",
                  "details": "許可されていないIPアドレスからのアクセスです。"
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "リソースが存在しない（未公開・非公開の場合を含む）。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "NotFound": {
                "value": {
                  "code": "E000007",
                  "message": "Not Found",
                  "details": "リソースが存在しません。"
                }
              }
            }
          }
        }
      },
      "NotFoundPreview": {
        "description": "プレビュー対象が存在しない（投稿プレビュー取得API）。存在秘匿のため、以下をすべて 404（`E000007`）に統一する。\n- プレビュー用トークンが不正（未知・形式不正）。\n- トークンの有効期限切れ（既定24時間）。\n- トークンが失効済み（投稿／サイト削除・管理者による明示失効）。\n- 対象不一致（トークンのスコープと `slug`／サイトが一致しない）。\n- 対象の下書き版が存在しない。\nレスポンスには `Cache-Control: no-store` を付与する。",
        "headers": {
          "Cache-Control": {
            "$ref": "#/components/headers/CacheControlNoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "NotFoundPreview": {
                "value": {
                  "code": "E000007",
                  "message": "Not Found",
                  "details": "リソースが存在しません。"
                }
              }
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "リクエストパラメータが不正。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "UnprocessableEntity": {
                "value": {
                  "code": "E000001",
                  "message": "Unprocessable Entity",
                  "details": "入力されたデータが不正です。",
                  "errors": {
                    "page": [
                      "The page must be at least 1."
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "InternalServerError": {
        "description": "サーバ内部エラー。",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "InternalServerError": {
                "value": {
                  "code": "E000003",
                  "message": "Internal Server Error",
                  "details": "サーバでエラーが発生しました。"
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "リクエスト上限を超過した（`E001003`）。以下のいずれか。\n- テナント単位（プラン別）のリクエスト上限を超過した。\n- フォーム送信API（`POST /api/v1/forms/{key}/submissions`）のスパム対策レートリミット（閲覧者IP 3回/分・フォーム 10回/10分）を超過した。\n`Retry-After` ヘッダで再試行までの待機秒数を返す。レスポンスには `Cache-Control: no-store` を付与する。",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "./error.yaml"
            },
            "examples": {
              "TooManyRequests": {
                "summary": "テナントのレート超過（プラン別）",
                "value": {
                  "code": "E001003",
                  "message": "Too Many Requests",
                  "details": "テナントのリクエスト上限（プラン別）を超過しました。"
                }
              },
              "FormSubmissionRateLimited": {
                "summary": "フォーム送信のレート超過（スパム対策）",
                "value": {
                  "code": "E001003",
                  "message": "Too Many Requests",
                  "details": "フォーム送信のレート上限（IP 3回/分・フォーム 10回/10分）を超過しました。"
                }
              }
            }
          }
        }
      }
    },
    "headers": {
      "CacheControlNoStore": {
        "description": "キャッシュ抑止（フォーム送信APIのレスポンスに付与）。常に `no-store`。",
        "schema": {
          "type": "string",
          "example": "no-store"
        }
      },
      "RetryAfter": {
        "description": "再試行までの待機秒数",
        "schema": {
          "type": "integer"
        }
      },
      "XRateLimitLimit": {
        "description": "単位時間あたりの上限リクエスト数（テナントのプラン別上限）",
        "schema": {
          "type": "integer"
        }
      },
      "XRateLimitRemaining": {
        "description": "現在の単位時間内で残り許容されるリクエスト数",
        "schema": {
          "type": "integer"
        }
      },
      "XRateLimitReset": {
        "description": "上限カウンタがリセットされる時刻（UNIXエポック秒）",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}