この記事は、microCMS Advent Calendar 2026の8日目の記事です。前日のMCP編に続き、本社の表示側を試します。
社員は私ひとりです。部署はあります。本社も作っています。
個人開発の屋号「After 6 Labo」で、ギフト紹介、古本屋のコラム、自作ツール、訪問者の要望でAIが改修するサイトを並行して作っています。それぞれの制作や運用を別々のAIに任せていたら、どこが何をしているのか、私が把握しづらくなりました。
そこで、各プロジェクトを部署として並べ、状況を見渡せる本社サイトを作ることにしました。人数より、組織図のほうが先に育っています。
部署の状態を画面へ出す部分は、先にスプレッドシートとGASで試していました。ただ、私の試作では安定させられず、どうするか考えていたところでした。
そこへ、前編のmicroCMS MCPです。AIから内容を登録し、項目ごとに保持できるところまで試して、「本社の報告を置く場所にも使えそうだ」と思いました。
試してみると、部署の入力項目・編集画面・取得APIをmicroCMSにまとめられました。管理画面で値を変えれば、本社サイトを再公開せず、次の取得で表示へ反映できます。報告を編集・保存する土台はmicroCMSに任せ、本社側には、閲覧者へ渡す情報の選別と報告の鮮度の判定を実装しました。
今回は、この表示部分を作った話です。CMSに報告の置き場所を用意すると、今度は「置いてある報告を、どう読めばいいのか」という問題が残ります。
画面が更新された時刻と、仕事が進んだ時刻は別です。取得に成功するたび、止まっている報告まで新しく見えてしまわないようにしました。
本社の見た目は、もう忙しそう
目指しているのは、こんな画面です。
最終的には、各部署のAIの作業状況に合わせて、ちびキャラが動き、指示書が飛び交う本社にする予定です。このGIFはその完成イメージのモックアップで、現時点では実際の作業とまだ連動していません。
見た目だけなら、すでに全社が忙しそうです。この勢いで眺め続けても、仕事の進捗は分からないので、まず本部とGiftの2部署だけで、表示の裏側を確かめます。
今回作ったのは、所有者限定の接続確認ページです。microCMSの値は管理画面から手動で変更しました。部署のAIから自動報告する部分と、画面の演出との連動は、この先の工程です。
前編の gifts は下書き登録の実験、今回の dept-status は部署の表示用です。商品紹介の下書きが、そのまま本社へ流れてくる構成ではありません。
microCMSには、掲示板に出す部分を置く
dept-status はリスト形式のAPIにしました。1部署につき1コンテンツで、次の項目を持ちます。
| フィールド | 意味 |
|---|---|
department |
部署名 |
taskTitle |
表示してよい作業名 |
status |
待機、受領、着手、完了、保留、失敗 |
reportedAt |
最後に報告した時刻 |
nextReportDueAt |
次の報告期限。未設定でもよい |
microCMSへ置くのは、見せてよい最新状態です。内部の詳しい指示や、受領から完了までの報告履歴を、そのまま掲示する設計にはしていません。
status は仕事の状態です。microCMS自体の「下書き/公開中」とは別で、CMSで公開保存すると取得対象になりますが、本社サイトの閲覧制限が外れるわけではありません。
ここまでなら、掲示板の項目を決めた話です。次は、掲示板を見に来た人へ、倉庫の鍵まで渡さないようにします。
fields は、鍵の代わりにならなかった
前編で作った gifts をAPIプレビューから取得すると、fields なしでは制作メモの editorialNotes も返ってきました。fields=id,name,giftReason を付ければ、紹介に使う項目だけになります。
ただし、fields を決めるのは呼ぶ側です。キーを持つ相手は、指定を外して取り直せます。これは取得項目を選ぶ機能なので、ブラウザーへキーを渡してから「この欄だけ読んでね」と指定しても、読み取り権限を狭めたことにはなりません。一覧取得APIの仕様
そこで、ブラウザーが呼ぶ表示用APIをサーバー側に置きました。
| 担当 | 行うこと | microCMSのキー |
|---|---|---|
| ブラウザー | 同じサイトの /api/microcms-status を呼ぶ |
持たない |
| Sitesのサーバー | 固定したAPIから取得し、5項目に組み立て直す | Secretで保持 |
| microCMS | 許可された取得へ応答する | キーの権限を確認する |
閲覧者には、受付で必要な情報だけを渡します。受付の奥にあるデータを、まるごと窓口へ積み上げないようにします。
今回のHobbyプランではAPIキーが1個なので、MCPと表示サーバーで同じキーを使いました。本社の表示を確かめたのは11月7日、前編の最後にPOSTを外したのは翌8日です。表示の確認時点では、サーバーのSecretにも gifts へPOSTできるキーが置かれていたことになります。ブラウザーへ読み取り用JSONだけを返しても、この権限は消えません。常用する書き込み経路では、用途別のキー分離を検討します。APIキーの権限と個数
受付が渡すものを、コードで固定する
本社はChatGPTのSitesで作っています。今回の環境では、サーバー処理と実行時のSecretを扱えたので、表示用APIも同じサイト内に置きました。サーバーの実行環境はCloudflare Workersです。
以下は表示APIの抜粋です。サービスIDとキーは実行時設定から読み、閲覧者が指定したURLや fields は使いません。
const url = new URL(
`https://${service}.microcms.io/api/v1/dept-status`
);
url.searchParams.set('limit', '10');
url.searchParams.set(
'fields',
'department,taskTitle,status,reportedAt,nextReportDueAt'
);
const response = await fetcher(url, {
headers: { 'X-MICROCMS-API-KEY': key },
redirect: 'manual',
cache: 'no-store',
signal: ctrl.signal,
});
if (
!response.ok ||
!response.headers.get('content-type')?.includes('application/json')
) {
throw Error('upstream');
}
fields で取得量を減らしたあと、返却値も組み立て直します。CMSのオブジェクトをそのまま返さないことが、表示側の境界になります。
// 部署名・状態・文字数・日時などを検証したあと
return {
department,
taskTitle,
status,
reportedAt,
nextReportDueAt,
};
実装では、許可した部署名と状態、部署の重複、文字列の長さ、報告時刻がサーバー時刻より1分を超えて未来になっていないか、報告時刻と期限の前後関係も確認しています。microCMSのセレクトフィールドは配列で返るので、状態が一つなら文字列へ直します。今は2部署・最大2件だけを受け付けるため、6部署へ広げるときは検証条件も変えます。
通信には15秒のタイムアウトを設け、失敗時は表示用の固定エラーを返します。上流の本文やヘッダーを、そのまま閲覧者へ渡すことはしません。
認証についても前提があります。元の実装で見ている oai-authenticated-user-id は、Sitesの所有者限定アクセスを前提にした値です。普通のWorkersで、利用者が付けられる同名ヘッダーの有無だけを確認しても認証にはなりません。 別環境へ移す場合は、その環境で検証できる認証と認可に置き換えます。
また、許可リストが守るのは項目の境界です。taskTitle に内部の連絡文を貼れば、その文も表示されます。見せてよい内容へ整える判断は、書き込む段階にも残ります。
存在するものが、返ってこないことを調べる
返却JSONに制作メモが見当たらなくても、もともとデータが空だっただけかもしれません。
そこで、dept-status に検査用の internalNote を追加し、無害な目印の文字列を入れました。同じキーで全項目を取得すると目印が存在することを確認したうえで、表示経路を調べています。
| 確認した場所・操作 | 結果 |
|---|---|
| 表示APIの実際の返却JSON | キーと目印を含まない |
| 配備先から取得したJS・CSS・SVG | キーと目印を含まない |
表示APIへ ?fields=internalNote を付ける |
HTTP 400。閲覧者からの項目指定を拒否 |
キーの検査では、値をログへ出さず、含まれているかだけを確認しました。ここで確かめたのは、この表示経路と配信資産です。
警備を厳しくしたら、受付まで止まった
さきほどの redirect: 'manual' は、最初からそうだったわけではありません。
キー付きの取得が別ホストへ転送されるのを避けようとして、最初は redirect: 'error' にしていました。Nodeのテストは6件とも成功。ところが、配備先ではCMSの応答を受け取る前に失敗します。
原因は、Workersが redirect: 'error' に対応していないことでした。Workersでは follow と manual を使い、error はRequestを構築する段階で TypeError になります。Cloudflare WorkersのRequest仕様
Node側では fetch をテスト用の関数へ差し替えていたため、その引数を実行環境が受け付けるかまでは調べられていませんでした。
「知らない建物へついていかないで」と頼んだつもりが、受付は玄関から一歩も出ていませんでした。これでは情報も漏れませんが、情報が来ません。
Miniflareからworkerdを動かし、外向きの応答だけを差し替えて確認しました。
| 上流の応答 | redirect: 'error' |
redirect: 'manual' |
|---|---|---|
| 正常な200 | 上流へ要求する前に失敗 | 取得に成功 |
| 別ホストへの302 | 上流へ届かず、転送の試験にならない | 最初の1回だけ取得し、追従せず表示APIは502 |
manual で最初の応答を受け取り、response.ok で3xxを失敗にする。これで、必要な取得は行い、転送先へはついていかない動きになりました。
外部APIを差し替えるテストで確認できるのは、自分の処理がどう判断するかです。配備先でそのリクエストを出せるかは、実行環境を通す試験も必要でした。
画面は、前の取得が終わってから3分待つ
今回の要件は、開いている本社画面で、数分おきに最新状態を見られることです。microCMSのWebhookは変更を受信先URLへ通知する機能で、その通知から開いているブラウザーの表示を更新する処理は別に必要になります。今回の検証では通知の受け口は追加せず、画面側から定期的に取り直す構成にしています。
画面は開いた直後に1回取得し、その処理が終わってから180秒後に次を予約します。遅い応答を待っている間に次の取得を重ねないよう、完了後に setTimeout で予約する形です。
本体の流れを、表示処理を省いた記事用の形にするとこうなります。
async function refresh() {
if (busy) return;
busy = true;
attemptAt = new Date().toISOString();
try {
const nextFeed = await readFeed();
feed = nextFeed; // 検証済みの成功応答だけを採用する
error = '';
} catch {
error = '通信失敗';
// feedは変えない。前回の内容と成功取得時刻を残す
} finally {
busy = false;
if (active) {
clearTimeout(timer);
timer = setTimeout(refresh, 180_000);
}
}
}
readFeed() は表示APIの取得と応答検証を担当します。実装では画面側にも20秒の通信タイムアウトを設け、画面を閉じると予約と通信を中止します。非表示から戻ったときにも取得し、重複呼び出しは busy で防いでいます。
ブラウザーが非表示になったときなどはタイマーが遅れるため、正確な3分周期を保証する仕組みではありません。MDNのポーリングとタイマーの説明
管理画面でGiftの値を変更し、画面を開いたまま待った結果がこちらです。
最初の成功取得は21:45:38.750、次は21:48:39.828。成功取得同士の間隔は181.078秒でした。その間に手動で変更した値が、次の取得で表示へ反映されています。再公開、リロード、取得ボタンの操作は挟んでいません。
これは取得間隔の観測です。「CMSで保存してから、必ず3分以内に反映される」という意味にはしません。
受付の巡回で、全員の出勤簿を更新しない
さて、取り直せるようにはなりました。
ここで取得のたびに報告時刻まで現在時刻へ変えると、同じ古い報告を何度読んでも、元気な新着報告になります。受付が社内便を集めて回るたび、全員が働いたことになる。社員ひとりの会社に、いきなり勤怠の問題が発生します。
時刻を、次のように分けました。
| 時刻 | 誰の何を表すか |
|---|---|
| 最終取得試行 | 画面が取得を試みた時刻。失敗しても進む |
fetchedAt |
サーバーが表示用データを取得・検証できた時刻。画面は最後に受け取れた成功分を保持 |
reportedAt |
CMSに登録された部署の報告時刻。取り直しても上書きしない |
nextReportDueAt |
次に報告が必要になる期限 |
報告が古いかの判定は、通信結果から独立させています。日時は表示APIで検証済みの値を使います。実装の判定をJavaScriptに抜き出すと、次の形です。
function reportHealth(item, now) {
if (!item.reportedAt) {
return { kind: 'muted', text: '未報告' };
}
if (item.status === '完了') {
return { kind: 'good', text: '完了報告済み' };
}
if (!item.nextReportDueAt) {
return { kind: 'muted', text: '次回報告期限が未設定' };
}
return now > Date.parse(item.nextReportDueAt)
? { kind: 'warning', text: '報告期限超過' }
: { kind: 'good', text: '報告期限内' };
}
未報告と期限未設定は分けます。期限がなければ、勝手に遅延と判定しません。完了報告済みなら、継続報告の期限判定から外します。期限ちょうどは期限内で、過ぎた時点から超過です。この画面では閲覧端末の時計を使って判定しています。
実際の画面でも、次の順番で確かめました。
- Giftの報告を17:15、次の期限を18:00にする。21時台に取得すると、通信は成功したまま、Giftは「報告期限超過」になる。
- 表示APIから試験用の503を返す。画面は「通信失敗」になるが、前回の内容と最終成功取得時刻は残る。最終取得試行だけが進む。
- 試験を解除する。取得は成功へ戻るが、17:15の報告と「報告期限超過」は残る。
障害試験の ?test=failure は、所有者限定の検証ページで使ったスイッチです。一般公開用へ移すときは、画面の試験操作とサーバーの分岐を外します。今回は実際の回線断やmicroCMSの障害を起こしたものではありません。
復旧したのは、報告を取りに行く経路です。古い報告を取り直せたからといって、新しい報告が生まれたわけではありません。この違いを、画面に残せました。
本社に、嘘の進捗を出させない
microCMSに置いた状態を、キーをブラウザーへ渡さずに表示し、開いたまま取り直すところまで確認できました。失敗したときは前回値を残し、その値が古いことも隠しません。
次は、部署のAIからの報告を dept-status へ反映する部分です。公開してよい内容への変換、書き込み用の権限、報告期限の決め方を用意します。GIFの6部署を動かす前に、まず2部署で本物の報告を受け取れるようにします。
スプレッドシートとGASで試していたところから、microCMSを使う案へ進めました。管理画面で報告を編集でき、表示の更新もできる。だからこそ、進んだと確認できない仕事を、画面の都合で進んだことにしないようにしました。
本社の看板は用意できました。あとは、そこへ本物の報告を届ける仕組みです。
「進捗どうですか」と聞かれる場所を、自分で作っています。
社員は、私ひとりです。
逃げ込めそうな部署は、全部、私の担当でした。
前編は、AIに書いてもらう側です。 「初めてのmicroCMS MCP、最後は『書けません』で成功した」では、Claude Codeからの下書き登録と権限の違いを扱います。掲載先は同じカレンダーの7日目です。
検証条件とAIの利用
表示と自動取得の確認は2026年11月7日、所有者限定の接続確認ページで行いました。本文中の時刻は日本時間です。MCPの初回実験は11月4日、追加の入力実験とPOST権限の撤去は11月8日なので、記事の掲載順と実験の順番は一部前後しています。
本社の実装・検証、構想GIFと資料作成にはChatGPTを使い、本稿の構成・推敲にはClaudeとChatGPTを使いました。掲載コードは実装の抜粋と、本文で明示した記事用の整理です。





