# Minion - 言語モデル向けドキュメント Minion は AI エージェントのワークスペースです。自前のホスト上で動くエージェント (ミニオン) を 管理し、エージェントと人が共有するワークスペース機能 - ノート、フォーム、カレンダー、 ドライブ、会計、ヘッドレス CMS - を持ちます。 このファイルには CMS の組み込みに要るページだけが入っています。各ページの先頭に正規 URL があります。 ドキュメント全文: https://docs.minionworkspace.com/ja/llms-full.txt English: https://docs.minionworkspace.com/llms-cms.txt ## 間違えやすい規約 コードを書く前にここを読んでください。**一見もっともらしい実装のほうが誤りになる**箇所です。 1. **CMS のコンテンツは、ビルド時に静的 JSON として読みます。リクエストごとではありません。** 公開されたエントリは公開オブジェクトストレージにファイルとして書き出されます。サイトの ビルド中にそのファイルを取得してください。SSR や ISR で取得してはいけませんし、ページの 描画に読み取り API (`/api/public/cms/...`) を使ってもいけません。あれは下書きの プレビューと書き込みのためのものです。 2. **公開 JSON の取得に失敗したら、ビルドを失敗させてください。** 例外を握って空の配列を 返すと、コンテンツが全部消えたサイトがそのまま公開されます。 3. **公開された CMS の JSON は誰でも読めます。** 認証はありません。API キーが守るのは 下書きと書き込みだけです。アクセス制御として説明してはいけません。 4. **フィールドの API ID が JSON のキーで、公開後は変更できません。** ラベルは自由に 変えられるので、ラベルからキーを推測しないでください。 5. **並び順と参照の展開はサーバー側で適用済みです。** 一覧を並べ替え直さないでください。 参照先のエントリは 1 段だけ展開されたオブジェクトとして入っています。 --- # CMS の組み込み Source: https://docs.minionworkspace.com/ja/guides/cms-integration/ Summary: Minion の CMS を静的サイトから読むために必要なことを、1 ページに閉じたもの。 このページは意図的に 1 ページで完結させてあります。[CMS ガイド](https://docs.minionworkspace.com/ja/guides/cms/)と重複する 内容も繰り返しているのは、**これだけを渡せばフロントを繋げる**状態にするためです (人に渡す場合も、コーディング支援の AI に渡す場合も)。 言語モデル向けのプレーンテキスト版は [`/ja/llms-cms.txt`](https://docs.minionworkspace.com/ja/llms-cms.txt) にあります。 ## この構成を 3 行で 公開すると、公開オブジェクトストレージにただの JSON ファイルが書き出されます。制作した サイトは**ビルド中に**そのファイルを読み、好きなように描画します。コンテンツが公開されたら、 Webhook がホスティング側にビルドをやり直させます。 サイトの配信に私たちは一切関与しません。だからアクセスが増えても料金が変わらず、私たちの 障害がクライアントのサイトを止めません。そして以下のすべての判断は、この一点から決まります。 ## 始める前に必要な値 3 つ。すべて Minion のサイトの **配信** タブにあります。 | 値 | 見た目 | どこから | |---|---|---| | 配信ベース URL | `https://.../` | 配信タブの先頭。**推測せず実物をコピーする** | | サイト ID | UUID | 同じタブと、ダッシュボードの URL `/cms/{siteId}` | | コンテンツ型の API ID | `news`、`posts` | コンテンツ型に付けた名前。JSON のファイル名そのもの | ベース URL とサイト ID は環境変数に置いてください。サイトを別のワークスペースへ譲渡しても この 2 つは変わらないので、引き渡しでコードを触る必要がありません。 ## URL ``` {base}/{siteId}/api/index.json サイトのコンテンツ型の一覧 {base}/{siteId}/api/{apiId}.json リスト型のエントリ一覧、またはオブジェクト型の単体 {base}/{siteId}/api/{apiId}/{entryId}.json 個別エントリ (id で引く) {base}/{siteId}/api/{apiId}/{slug}.json 同じエントリ (slug で引く) ``` 4 つともただのファイルです。`GET` のみ、ヘッダ不要、認証なし。 ## レスポンスの形 **リスト型**はページング形式です (読み取り API と同じ形なので、同じコードがどちらにも使えます)。 ```json { "contents": [ /* エントリ */ ], "totalCount": 12, "offset": 0, "limit": 12 } ``` **オブジェクト型**と**個別エントリのファイル**は、エントリそのものです (包まれません)。 **すべてのエントリ**が共通のシステムキーを持ち、そのあとに定義したフィールドが API ID を キーとして並びます。 ```json { "id": "1d5f21ef-…", "slug": "hello-world", "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z", "publishedAt": "2026-01-01T00:00:00.000Z", "sortOrder": 0, "title": "こんにちは", "body": "# こんにちは\n…" } ``` `slug` は未設定なら `null` です。URL を組むときは `id` にフォールバックしてください。 ## フィールド型と JSON の対応 | フィールド型 | JSON | |---|---| | テキスト / テキストエリア | `"…"` | | リッチテキスト | `"…"` — **Markdown** です (HTML ではありません) | | 数値 | `0` | | 真偽値 | `true` | | 日付 | `"2026-01-01"` | | セレクト | `"news"` — 選択肢の**ラベル**が入ります (内部の値ではありません) | | 画像 / ファイル | オブジェクト (下記) | | コンテンツ参照 | `{ "id": …, "slug": … }` — 参照先を 1 段展開したもの | | 繰り返し | `[ { … } ]` — オブジェクトの配列 | | 埋め込み URL | `"https://…"` — YouTube / Vimeo の URL。動画は預かりません | **複数値**にしたフィールドは、同じ形の配列になります。 画像フィールドはファイル全体に展開されます。 ```json { "id": "…", "url": "https://…", "object_key": "…", "filename": "cover.jpg", "mime_type": "image/jpeg", "byte_size": 148213, "width": 1200, "height": 630, "alt": "…", "variants": [{ "width": 800, "format": "webp", "url": "https://…" }] } ``` `variants` はアップロード時に生成済みの縮小版です。原本を全端末に配らず、`srcset` に 並べてください。 ## 保証されていること テンプレート側で身構える必要はありません。 - **並び順は適用済みです。** 編集画面と同じ順序 (手動の並び替え → 公開日の新しい順) で 出ます。並べ替え直さないでください。 - **参照は 1 段だけ展開されます。** 参照先はオブジェクトとして入ります。未公開や 1 段より 深い場合は `null` ではなく `{ "id": "…" }` になるので、`ref.id` を読んでも落ちません。 - **下書きは入りません。** 未公開のエントリはこれらのファイルに一切書き出されません。 - **一覧に出ているエントリには必ず個別ファイルがあります。** 個別ファイルは一覧ファイルより 先に書かれます。 - **メディアの URL は絶対 URL**なので、そのまま `src` に入れられます。 ## 実装例 ### Astro ```astro --- // src/pages/news/index.astro const CMS_BASE = process.env.CMS_BASE const SITE_ID = process.env.CMS_SITE_ID const API_ID = 'news' const res = await fetch(`${CMS_BASE}/${SITE_ID}/api/${API_ID}.json`) if (!res.ok) throw new Error(`CMS fetch failed: ${res.status} ${API_ID}.json`) const { contents } = await res.json() --- ``` ```astro --- // src/pages/news/[slug].astro import { marked } from 'marked' export async function getStaticPaths() { // getStaticPaths はビルド時に単独で実行されるため、この frontmatter の他の場所で // 宣言した変数を見られない。環境変数はこの中で読み直す。 const CMS_BASE = process.env.CMS_BASE const SITE_ID = process.env.CMS_SITE_ID const res = await fetch(`${CMS_BASE}/${SITE_ID}/api/news.json`) if (!res.ok) throw new Error(`CMS fetch failed: ${res.status} news.json`) const { contents } = await res.json() // 一覧に全フィールドが入っているので、そのまま props で渡す。ここで個別ファイルを // 取りに行くと、記事数ぶんリクエストが増えるだけで得るものが無い。 return contents.map((entry) => ({ params: { slug: entry.slug ?? entry.id }, props: { entry }, })) } const { entry } = Astro.props const body = marked.parse(entry.body ?? '', { async: false }) const cover = entry.cover ?? null ---

{entry.title}

{cover && ( `${v.url} ${v.width}w`).join(', ')} alt={cover.alt ?? ''} /> )}
``` `import.meta.env` ではなく `process.env` を使っているのは、前者に出るのは `PUBLIC_` 付きの 値と `.env` ファイルの内容だけで、CI はたいてい本物の環境変数として渡してくるためです。 ### Next.js (App Router) ```tsx // app/news/page.tsx export const dynamic = 'force-static' const CMS_BASE = process.env.CMS_BASE! const SITE_ID = process.env.CMS_SITE_ID! export default async function NewsIndex() { const res = await fetch(`${CMS_BASE}/${SITE_ID}/api/news.json`) if (!res.ok) throw new Error(`CMS fetch failed: ${res.status} news.json`) const { contents } = await res.json() return ( ) } ``` ```tsx // app/news/[slug]/page.tsx export const dynamic = 'force-static' export const dynamicParams = false const CMS_BASE = process.env.CMS_BASE! const SITE_ID = process.env.CMS_SITE_ID! async function list() { const res = await fetch(`${CMS_BASE}/${SITE_ID}/api/news.json`) if (!res.ok) throw new Error(`CMS fetch failed: ${res.status} news.json`) return res.json() } export async function generateStaticParams() { const { contents } = await list() return contents.map((entry: any) => ({ slug: entry.slug ?? entry.id })) } export default async function NewsEntry({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params const res = await fetch(`${CMS_BASE}/${SITE_ID}/api/news/${slug}.json`) if (!res.ok) throw new Error(`CMS fetch failed: ${res.status} news/${slug}.json`) const entry = await res.json() return

{entry.title}

} ``` `dynamicParams = false` にすると、知らない slug へのリクエストが実行時レンダリングではなく 404 になります。一覧に無い slug は存在しないので、こちらが正しい挙動です。 ### Nuxt ```ts // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { cmsBase: process.env.CMS_BASE, cmsSiteId: process.env.CMS_SITE_ID, }, // 一覧をプリレンダリングし、そこからのリンクを辿って各記事ページを生成する。 nitro: { prerender: { crawlLinks: true, routes: ['/news'] } }, }) ``` ```vue ``` ビルドは `nuxi generate` です。 ## エラーの扱い **取得に失敗したらビルドを失敗させてください。** 上の例がすべて空配列ではなく例外を投げて いるのは意図的です。ビルド中に CMS が一時的に届かなかったとき、例外を握ると**記事が全部 消えたサイトがデプロイされます**。投げれば前回のデプロイがそのまま残ります。 個別エントリのファイルが無い場合も同じです。空白のページを描かず、投げてください。 ## 公開されたら作り直す 静的にビルドしたサイトは、公開に気づきません。ホスティング側でビルドフックを作り (Vercel なら **Settings → Git → Deploy Hooks**、Netlify なら **Site configuration → Build & deploy → Build hooks**)、その URL を Minion の **配信 → Webhook** に貼ってください。 貼ったらその場で **テスト** を押すこと。届かない Webhook は、いま気づくのが一番簡単です。 イベントは `entry.published` / `entry.unpublished` / `entry.deleted` / `content_type.updated` / `site.published`。ひとつも選ばなければ全部が届きます。ペイロードは JSON で `x-cms-event` ヘッダが付きます。**署名を付ける**を有効にすると `x-cms-signature` (生のボディの HMAC-SHA256) も付きます。 Webhook が失敗しても公開は巻き戻りません。ビルドが壊れてもコンテンツは正しく公開されて いるので、ビルドを直して回し直せば済みます。 ## 下書きをプレビューする 下書きは静的ファイルに出ないので、プレビューだけが私たちに直接問い合わせる唯一の経路です。 ```bash curl -H "X-CMS-API-KEY: YOUR_READ_KEY" \ "https://minionworkspace.com/api/public/cms/{siteId}/news?draftKey=YOUR_DRAFT_KEY" ``` どちらのキーも **配信** タブにあります。クエリは microCMS 互換のサブセット (`limit` / `offset` / `orders` / `fields` / `filters` / `depth` / `q`) で、読み取りは サイトあたり毎分 300 リクエストに制限されています。 「設定」の **プレビュー URL** に自分のサイトのプレビュー用ページを設定しておくと、編集画面の **プレビュー**から生の JSON ではなくそのページが開きます。 **プレビューと CI からの更新にだけ使ってください。公開ページの描画には使いません。** ## 公開されたファイルを読めるのは誰か URL を知っている人は誰でもです。公開済みのコンテンツに認証はありませんし、付けられません。 訪問者のトラフィックが私たちに来ないのはそのためです。 サイト ID も秘密ではありません。メディアの URL に含まれ、制作したサイトの HTML にそのまま 入ります。ページを見た人は `index.json` を読めるので、**リンクしていない公開済みエントリを 含めて全部列挙できます**。**「公開したがまだリンクしていない」は非公開ではありません。** そういうコンテンツは未公開のままにして、**予約公開**を使ってください。 API キーが守るのは下書きと書き込みです。すでに公開したものへのアクセス制御ではありません。 ## やってはいけないこと | ❌ | ✅ | なぜ | |---|---|---| | ページの描画に読み取り API (`/api/public/cms/…`) を使う | ビルド時に静的 JSON を読む | 私たちの遅延がクライアントの遅延に、私たちの障害がクライアントの障害になる | | fetch を `try { … } catch { return [] }` で囲む | 投げてビルドを落とす | 握るとコンテンツの無いサイトがデプロイされる | | テンプレートで `contents` を並べ替える | 返ってきた順に描く | 並び順は適用済み。並べ替えると編集者の手動並び替えが黙って無視される | | すべての画像に `cover.url` を使う | `variants` から `srcset` を組む | 原本は 1200px 幅のこともあり、それをスマホにも配ることになる | | 配信ベース URL を直書きする | 環境変数から読む | 配信ドメインは動きうる唯一の値 | | フィールドのラベルから JSON のキーを推測する | フィールドの API ID を使う | ラベルは自由に変わる。API ID は公開後に固定される | | API キーをアクセス制御として扱う | 公開済みは公開だと扱う | 静的ファイルに認証は無い | | `getStaticPaths` から個別ファイルを取りに行く | 一覧のエントリを props で渡す | 一覧に全フィールドが入っている。個別取得はリクエストが増えるだけ | --- # CMS Source: https://docs.minionworkspace.com/ja/guides/cms/ Summary: コンテンツの管理をクライアントに渡し、制作したサイトは静的 JSON として読み込む CMS は、Web サイトのコンテンツ管理をその持ち主に渡すための機能です。クライアントはここで編集し、ページを描画するのは制作したサイトのままです。 ヘッドレス CMS なので、テーマもテンプレートもフロントエンドも持ちません。コンテンツを公開すると、Minion が公開ストレージにただの JSON ファイルを書き出します。制作したサイトはそのファイルを (多くの場合ビルド時に) 読み、好きなように描画します。 この切り分けが設計のすべてです。 - **閲覧者が当社を経由しません。** サイトが読むのは静的ファイルなので、アクセスが増えても料金は変わらず、当社の障害がクライアントのサイトの障害になりません。 - **リクエスト数で課金しません。** リクエストが当社に来ないので、そもそも計測する対象がありません。 - **いつでも卒業できます。** 公開されているものは既にただの JSON ファイルです。書き出し (エクスポート) を使えば、そのファイル群とスキーマと画像が 1 つの ZIP で手に入ります。 CMS は experimental で、既定では無効です。サイドバーに出ていない場合はお問い合わせください。 :::tip[フロントを繋ぐ作業をしますか?] [CMS の組み込み](https://docs.minionworkspace.com/ja/guides/cms-integration/) は、URL のパターン・JSON の形・Astro / Next.js / Nuxt の実装例・やってはいけないことを 1 ページに閉じたものです。 **そのページだけを開発者 (あるいはコーディング支援の AI) に渡せば繋げる**ように書いてあります。 言語モデル向けのプレーンテキスト版は `/ja/llms-cms.txt` にあります。 ::: ## 5 分でサイトに繋ぐ **1. サイトを作る。** 案件ごとに 1 つ作ります。あとで複製・譲渡・削除する単位になります。 **2. コンテンツ型を作る。** コンテンツ型は「扱うコンテンツの種類」です。ブログ記事、お知らせ、スタッフ紹介など。形を選びます。 | 種類 | 用途 | 公開される形 | |---|---|---| | リスト | 同じ形のエントリを複数持つもの (記事・お知らせ・商品) | エントリの配列 | | オブジェクト | 1 件しかないもの (サイト設定・トップページ) | 単体のオブジェクト | あわせて **API ID** (`posts`、`news` など) を決めます。これが公開される JSON のファイル名になるため、あとから変更できません。 **3. フィールドを定義する。** 各フィールドは **ラベル** と **API ID** を持ちます。ラベルは編集者に見える名前で、いつでも変えられます。API ID は公開 JSON のキーになるため、スキーマを公開したあとは固定されます。スキーマビルダーには、定義したフィールドがどんな JSON になるかのプレビューが出ます。 **4. エントリを書いて公開する。** 下書きの保存は公開面を一切変えません。公開すると、その版が固定され、ファイルが書き出されます。 **5. サイトの公開を開始する。** 公開は公開ストレージへの書き出しなので、先にワークスペースへ決済手段を登録していただきます。**課金はしません。** 誰でも無料で使える匿名の公開ストレージは、構造的にフィッシングやマルウェアの置き場になるための措置です。 **6. サイトから読み込む。** 「配信」タブに、そのサイトのコンテンツ型ごとの URL がコピーボタン付きで並びます。`fetch` / Next.js / Astro のコード例もそのまま貼れます。 ```js const res = await fetch('https://…/{siteId}/api/posts.json') const { contents } = await res.json() ``` ## 配信される JSON の形 公開すると、コンテンツ型ごとに 1 ファイル、エントリごとに 1 ファイル、そしてマニフェストが書き出されます。 | ファイル | 内容 | |---|---| | `/api/index.json` | サイトのコンテンツ型の一覧 (フロントがどの API があるかを知るため) | | `/api/{apiId}.json` | リスト型のエントリ一覧、またはオブジェクト型の単体 | | `/api/{apiId}/{entryId}.json` | 個別エントリ (id で引く) | | `/api/{apiId}/{slug}.json` | 同じエントリ (slug で引く) | リスト型のファイルはページング形式のレスポンスです。読み取り API と同じ形なので、同じコードがどちらにも使えます。 ```json { "contents": [ /* エントリ */ ], "totalCount": 12, "offset": 0, "limit": 12 } ``` エントリは共通のシステムキーを持ち、そのあとに定義したフィールドが API ID をキーとして並びます。 ```json { "id": "…", "slug": "hello-world", "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z", "publishedAt": "2026-01-01T00:00:00.000Z", "sortOrder": 0, "title": "こんにちは", "body": "# こんにちは\n…" } ``` テンプレートを書く前に知っておくと良いことが 2 つあります。 - **並び順は適用済みです。** エントリ一覧の画面と同じ順序 (手動の並び替え → 公開日の新しい順) で出ます。 - **参照は 1 段だけ展開されます。** 参照先のエントリはオブジェクトとして入ります。未公開だったり 1 段より深い場合は `null` ではなく `{ "id": "…" }` になるので、`ref.id` を読むテンプレートが落ちることはありません。 ### このファイルを読めるのは誰か URL を知っている人は誰でも読めます。公開済みのコンテンツに認証はありませんし、付けられません。訪問者のリクエストが私たちのところに来ないのはそのためで、アクセスが増えても料金が変わらず、私たちの側の障害がクライアントのサイトを止めないのもここから来ています。 計画に織り込んでおくべき点が 2 つあります。 - **サイト ID は秘密ではありません。** メディアの URL に含まれ、制作したサイトの HTML にそのまま入ります。ページを見た人は `/api/index.json` を読めるので、そこから**すべてのコンテンツ型と公開済みエントリを列挙できます** (フロントがリンクしていないものも含みます)。 - **「公開したがまだリンクしていない」は非公開ではありません。** ある日付まで読まれては困るものは、先に公開してあとからリンクするのではなく、未公開のまま**予約公開**を設定してください。 下書きは影響を受けません。未公開のエントリはこれらのファイルに一切書き出されません。**API キーが守るのは読み取り API (下書きと書き込み) であって、公開済みのコンテンツではありません。** 公開したものへのアクセス制御として扱わないでください。 ## フィールド型 | 型 | JSON | 補足 | |---|---|---| | テキスト | `"…"` | 1,000 文字まで | | テキストエリア | `"…"` | 20,000 文字まで | | リッチテキスト | `"…"` | 実体は Markdown。200,000 文字まで | | 数値 | `0` | | | 真偽値 | `true` | | | 日付 | `"2026-01-01"` | | | 選択肢 | `"news"` | 内部の値ではなく**ラベル**が出ます | | 画像 / ファイル | `{ … }` | メディアオブジェクト (下記) | | コンテンツ参照 | `{ "id": …, "slug": … }` | 他のエントリ。1 段展開 | | 繰り返し | `[ { … } ]` | オブジェクトの配列。ネストは 1 段、200 行まで | | 埋め込み URL | `"https://…"` | YouTube / Vimeo の URL。動画ホスティングは持ちません | 「複数」を有効にしたフィールドは、同じ形の配列になります。 メディアのフィールドはファイル情報に展開されます。 ```json { "id": "…", "url": "https://…", "object_key": "…", "filename": "cover.jpg", "mime_type": "image/jpeg", "byte_size": 148213, "width": 1200, "height": 630, "alt": "…", "variants": [{ "width": 800, "format": "webp", "url": "https://…" }] } ``` `variants` はアップロード時に生成した縮小版です。そのまま使っても、`srcset` に並べても構いません。 ### 名前を安全に変える フィールドは不変の内部 ID で、選択肢は不変の値で保存されています。つまり: - **ラベルの変更はいつでも安全です。** 公開中のサイトは何も変わりません。 - **API ID はスキーマを公開したあと変更できません。** フロントが読んでいるキーそのものだからです。 - **選択肢のラベルを変えると公開 JSON が変わります。** 公開されるのはラベルのほうなので、コンテンツの編集として扱ってください。 - **スキーマを変えても既存のエントリは壊れません。** 各エントリは自分が書かれた版のスキーマで解釈されます。削除したフィールドは出力に出なくなるだけです。 ## 下書きをプレビューする 下書きは静的ファイルには一切書き出されません。「編集した瞬間に公開ページが壊れる」ことがないのはこのためです。未公開の内容を表示したいときは、読み取り API を使います。 ```bash curl -H "X-CMS-API-KEY: YOUR_READ_KEY" \ "https://minionworkspace.com/api/public/cms/{siteId}/posts?draftKey=YOUR_DRAFT_KEY" ``` 読み取りキーは「配信 → API キー」で発行します。下書きキーも同じタブにあり、漏れた場合は再発行できます。このキーがあるのは、このエンドポイントが下書きを返せるためです。**公開済みのコンテンツを守っているものではありません** (公開済みのファイルは URL を知っていれば誰でも読めます) (`X-MICROCMS-API-KEY` も受け付けるので、microCMS 向けに書いたフロントはこの部分を変えずに済みます)。 「設定」の **プレビュー URL** に制作したサイトのプレビュー用ページを設定しておくと、編集画面の「プレビュー」から生の JSON ではなくそのページが開きます。 読み取り API のクエリは microCMS 互換のサブセットです。 | パラメータ | 例 | | |---|---|---| | `limit` / `offset` | `limit=10&offset=20` | 既定 10、最大 100 | | `orders` | `orders=-publishedAt,title` | `-` で降順 | | `fields` | `fields=id,title` | 応答を絞る | | `filters` | `filters=category[equals]news[and]title[contains]告知` | `equals` / `not_equals` / `contains` / `begins_with` / `exists` / `not_exists` を `[and]` `[or]` で連結。左から順に評価 | | `depth` | `depth=2` | 参照の展開段数。最大 3 | | `q` | `q=キーワード` | エントリ全体への全文検索 | レート制限はサイトごとに読み取り 300 回 / 分、書き込み 60 回 / 分です。 :::caution 読み取り API は、プレビューと、CI やミニオンからの更新のために使ってください。表示のたびに呼ぶ構成 (SSR / ISR) にすると、当社の遅延がクライアントの遅延に、当社の障害がクライアントの障害になります。ページの描画にはビルド時に読んだ静的ファイルを使ってください。 ::: ## 公開のたびに再ビルドする 静的に生成するサイトでは、コンテンツを公開しただけでは半分です。サイト側の再ビルドが要ります。Webhook はそのためにあります。 まずホスティング側でビルドフックを作り (Vercel は **Settings → Git → Deploy Hooks**、Netlify は **Site configuration → Build & deploy → Build hooks**)、その URL を「配信 → Webhook」に貼ります。以降は公開のたびにビルドが走ります。登録したらすぐ **テスト** を送ってください。届いていない Webhook に気づくなら、来週ではなく今のほうが安上がりです。 購読するイベントは選べます。空のままにすると全イベントを受け取ります。 `entry.published` · `entry.unpublished` · `entry.deleted` · `content_type.updated` · `site.published` ペイロードは JSON で、`x-cms-event` ヘッダ付きで送られます。 ```json { "event": "entry.published", "site_id": "…", "content_type": "posts", "entry_id": "…", "entry_slug": "hello-world", "occurred_at": "2026-01-01T00:00:00.000Z" } ``` **ペイロードに署名する** を有効にすると `x-cms-signature` ヘッダが付きます。作成時に一度だけ表示されるシークレットで、本文そのものを HMAC-SHA256 したものです。ビルドを叩く以上のことをする受け口なら検証してください。 Webhook の失敗で公開が巻き戻ることはありません。ビルドが落ちても、コンテンツは正しく公開されたままです。ビルドを直して再度発火させてください。 ### 予約公開 エントリには **公開日時** と **公開終了日時** を設定できます。実行は 5 分間隔なので、「その時刻から 5 分以内」と考えてください。予約公開も手動の公開と同じ検証を通ります。必須項目が空のときは、予約を黙って取り下げずに失敗として報告するので、エントリを直せば次の実行で公開されます。 ## 上限と、超えたときの挙動 サイト数とメディアの容量はワークスペース単位で数えます。 | プラン | サイト数 | メディア容量 | |---|---|---| | Free | 2 | 1 GB | | Starter | 5 | 10 GB | | Team | 20 | 50 GB | | Business | 100 | 200 GB | | Enterprise | 無制限 | 1 TB | 上限を超えて止まるのは**新規アップロードだけ**です。既に公開しているファイルは配信され続け、公開の操作も通ります。容量を理由にクライアントの稼働中のサイトを落とすことはしません。 ## 誰が何をできるか ワークスペースのメンバーは全員サイトを見られます。ただし、許可するまで誰も変更できません。 | 権限 | できること | |---|---| | 管理者 | すべて (公開の開始停止・API キー・Webhook・アクセス権・サイトの削除) | | 編集できる | コンテンツ型とスキーマの変更、および執筆者ができること全部 | | 執筆できる | エントリとメディア (執筆・公開・アップロード) | | 閲覧のみ | 読み取り。**ワークスペースメンバーの既定** | 管理者は自動で決まります (ワークスペースの owner・admin と、サイトを作った本人)。残りの 2 つは サイトごとに**設定 → アクセス権**で付与します。付与を外すと、その人は閲覧のみに戻ります。 コンテンツを書く人には**執筆できる**を、構造を変えてよい人にだけ**編集できる**を付与してください。 フィールドを削除すると、そのエントリを次に公開した時点で公開 JSON から消えます。執筆担当者が うっかり踏んでよい操作ではありません。 ### 組み込み用の画面を畳む サイトを繋ぎ終えると、配信タブとスキーマタブは役目を終え、コンテンツを書く人にとっては ノイズになります。**設定 → 組み込みモード**でこれを畳むと、執筆に必要な画面だけになります。 引き渡す前にオフにし、繋ぎ方を変えるときにまたオンにしてください。 変わるのは表示だけで、誰に何が許されるかは変わりません。サイトを守っているのは上の権限のほうです。 執筆できる・閲覧のみの人には、この設定に関係なくこれらのタブは出ません。 ## サイトを複製する / 譲渡する **複製**は、同じコンテンツ型・同じスキーマ・同じ API ID を持つ新しいサイトを作ります (エントリと画像も含めるか選べます)。複製先は必ず非公開で始まり、エントリは下書きになります。API キーと Webhook は意図的に引き継ぎません。破壊的なスキーマ変更を試すときや、前の案件の構成を次の案件で使い回すときに使ってください。 **譲渡**はサイトを別のワークスペースへ移します。相手のワークスペーススラッグを入力すると、相手の管理者が受け入れた時点で移ります。サイトの公開面の識別子は何も変わりません。メディアの URL も API のパスも API キーも Webhook も、ワークスペースではなくサイトに紐づいているためです。稼働中のサイトを止めずに渡せますし、クライアントの CI の環境変数を書き換える必要もありません。 計画に織り込んでおくとよい点が 2 つあります。サイトごとのアクセス権は破棄されます (旧ワークスペースのメンバーを指していたため)。既定は閲覧のみなので、受け取り側は管理者が自分たちのメンバーに権限を付与するまで、管理者以外は編集できません。そして譲渡先のプラン上限を超えていても譲渡は成立します — 既存のファイルは配信され続け、止まるのは新規アップロードだけです。 ## すべて書き出す **書き出し**は、下書きを含む全コンテンツ・中立的な形式のスキーマ定義・メディアを 1 つの ZIP にまとめます。 `published/` フォルダには、配信していた JSON がそのままの形で入っています。移行の途中でも、このファイル群を任意の静的ホスティングに置けばサイトは動き続けます。 メディアの同梱は 200 MB で打ち切り、超えた分はマニフェストに URL 参照として載せます。打ち切りは README・マニフェスト・レスポンスヘッダに必ず明記され、黙って減らされることはありません。