Claude CodeでmicroCMSと連携したサイトを作る — スキーマから型を作る
Claude CodeでmicroCMS連携のNext.jsサイトを作るとき、APIキーを.envに隔離する設定と、APIスキーマJSONからTypeScriptの型を作って検証する手順をまとめます。
microCMSと連携するサイトをClaude Codeで作るなら、先に決めておくことが2つあります。APIキーをClaudeの目に触れさせない設定と、コンテンツの型をどこから作るかです。この記事では、Next.jsのApp Routerで一覧を表示するところまでを、この2点を軸に進めます。
型の元になるのは、microCMSが書き出せるAPIスキーマのJSONです。型を推測で書かせず、スキーマから機械的に作り、tsc で検証する流れにします。
microCMS連携は3段で進める
microCMS連携サイトの作業順
- 1
キーの置き場所を決める
.env系のファイルにだけ置き、Claude Codeのpermissions.denyで読み取りを止めます。 - 2
スキーマを書き出す
管理画面のエクスポート、またはマネジメントAPIでJSONを手に入れます。
- 3
型を作って検証する
JSONから型を生成し、取得コードを
tsc --noEmitに通します。
順序が大事です。コードを書かせてから型を直すより、スキーマを渡して型を先に作るほうが、フィールド名の取り違えが減ります。
APIキーを.envに隔離する
microCMSのAPIキーは、リクエストヘッダー X-MICROCMS-API-KEY に載せる認証キーです。作成・編集・削除ができるのは管理者権限のユーザーだけで、プランごとに作れる個数が違います(Hobbyは1個、Teamは3個、Businessは10個、Enterpriseは20個)。
ここで押さえておきたいのが、キーの権限設計です。microCMSのドキュメントには、次の注意書きがあります。クライアントから直接APIを呼ぶ構成だと、外部のユーザーがキーを把握できます。エンドポイントが分かれば任意のAPIを呼べるため、必要最低限のGET権限だけを付けるか、サーバー側から呼んでキーを隠すか、どちらかにします。
Next.jsでは、NEXT_PUBLIC_ で始まらない環境変数はブラウザに出ません。付けた変数はビルド時にJavaScriptへ埋め込まれます。microCMSのキーに NEXT_PUBLIC_ を付けると、前述の「キーが露出する構成」そのものになります。
Claude Codeの設定
キーは .env.local のようなファイルに置き、Claude Codeからは読めなくします。プロジェクトの .claude/settings.json に次の permissions.deny を書きます。
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.local)",
"Read(./.env.production)"
]
}
}Read のdenyルールは、ファイル探索と検索の結果からも該当ファイルを外し、同じパスへのEditとWriteも止めます。つまり、Claudeに .env.local を作らせることはできません。自分で作るか、中身のない .env.example を別に用意します。
Read(./.env.*) のようなワイルドカードで一括指定すると、.env.example まで読めなくなります。雛形を読ませたいなら、名前を列挙する書き方が安全です。
.claudeignore というファイルは効果がないので、除外したいパスは Read のdenyルールに移します。
取得コードからキーを渡す
microCMSのNext.js(App Router)向けGetting Startedでは、microcms-js-sdk の createClient に serviceDomain と apiKey を渡します。ドキュメントのサンプルは文字列を直接書いていますが、環境変数から読む形にします。
// libs/client.ts
import { createClient } from 'microcms-js-sdk';
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
apiKey: process.env.MICROCMS_API_KEY!,
});serviceDomain は、サービスのURL https://XXXX.microcms.io の XXXX の部分です。このファイルをサーバーコンポーネントからだけ読み込めば、キーはブラウザに届きません。Getting Startedでも、サーバーコンポーネントの処理は生成されるJavaScriptに含まれないと説明されています。
スキーマJSONを手に入れる
型の元になるスキーマは、3通りの方法で入手できます。
| 方法 | 手順 | 向く場面 |
|---|---|---|
| 管理画面のエクスポート | 手順「API設定」→「APIスキーマ」→「この設定をエクスポートする」 | 向く場面一度きりの取得 |
| マネジメントAPI | 手順GET /api/v1/apis/{endpoint} | 向く場面変更のたびに取り直したい |
| microCMS MCP | 手順microcms_get_api_info ツール | 向く場面Claudeに直接読ませたい |
管理画面のエクスポートは、権限によっては画面に到達できないことがあります。その場合は権限管理の状態を見るか、サービスの管理者に頼みます。
マネジメントAPIのベースURLは https://{SERVICE_ID}.microcms-management.io で、コンテンツAPIとはドメインが違います。ベータ版として提供されており、APIキー側で「API情報の取得」を有効にしておく必要があります。レスポンスは、エクスポートしたJSONと基本的に同じ形です。
JSONの構造は、通常フィールドの配列 apiFields と、カスタムフィールドの配列 customFields の2つです。各フィールドは fieldId、name、kind、required などを持ちます。kind が型を決める手がかりになります。
フィールドの種類と型の対応
「GET APIのフィールドごとのレスポンス形式」のページに、各フィールドが返す値の型と、空のときの形が載っています。型の対応は次のとおりです。
kind | レスポンスの型 | 空のとき |
|---|---|---|
text / textArea | レスポンスの型文字列 | 空のときキーなし、または null |
richEditorV2 | レスポンスの型文字列(HTML) | 空のときキーなし、または null |
number | レスポンスの型数値 | 空のときキーなし、または null |
date | レスポンスの型ISO 8601のUTC文字列 | 空のときキーなし |
boolean | レスポンスの型真偽値 | 空のときfalse |
media | レスポンスの型画像オブジェクト | 空のときキーなし |
mediaList | レスポンスの型画像オブジェクトの配列 | 空のとき[] |
select | レスポンスの型選択肢の配列 | 空のとき[] |
relation | レスポンスの型参照先コンテンツのオブジェクト | 空のときnull |
relationList | レスポンスの型参照先コンテンツの配列 | 空のとき[] |
custom | レスポンスの型オブジェクト(fieldId を含む) | 空のときnull、または fieldId のみ |
repeater | レスポンスの型カスタムフィールドの配列 | 空のとき[] または null |
見落としやすいのは、select が単一選択でも配列で返る点です。複数選択をオフにしていても、レスポンスは ["Apple"] の形になります。型を string と書くと、実行時に食い違います。
もう1つは boolean です。値が未入力でも false が返るので、省略可能な型にする必要はありません。
画像オブジェクトは url を必ず持ち、alt は任意項目なのでキーごと無いことがあります。width と height も、拡張子によってはキーが含まれません。SDKには MicroCMSImage、MicroCMSContentId、MicroCMSDate といった型が用意されているので、これらを使い回します。
file と iframe のフィールドは、この記事のスクリプトでは unknown にしています。旧エディタの richEditor(V2ではないもの)も表とスクリプトに含めていないので、同じく unknown に落ちます。file のレスポンスはURLとファイルサイズを含むオブジェクトですが、使う場面で型を足すほうが確実です。
スキーマJSONから型を作るスクリプト
APIスキーマのエクスポートのページには、スキーマから型を作る手順が載っていません。そこで、JSONを読んで型を出力する小さなスクリプトをClaude Codeに書かせ、手元で動かして確かめる形にします。次は、上の対応表をそのまま実装した例です。
// scripts/gen-types.mjs
import { readFileSync } from 'node:fs';
const [file, typeName = 'Content'] = process.argv.slice(2);
const schema = JSON.parse(readFileSync(file, 'utf8'));
const pascal = (s) => s.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
const REL = (f) =>
`${pascal(f.referencedApiEndpoint ?? 'unknown')} & MicroCMSContentId & MicroCMSDate`;
const base = (f) => {
switch (f.kind) {
case 'text': case 'textArea': case 'richEditorV2': case 'date': return 'string';
case 'number': return 'number';
case 'boolean': return 'boolean';
case 'media': return 'MicroCMSImage';
case 'mediaList': return 'MicroCMSImage[]';
case 'select':
return `(${(f.selectItems ?? []).map((i) => JSON.stringify(i.value)).join(' | ') || 'string'})[]`;
case 'relation': return REL(f);
case 'relationList': return `(${REL(f)})[]`;
case 'custom': return pascal(f.customFieldId ?? 'unknown');
case 'repeater': return `(${(f.customFieldIds ?? []).map(pascal).join(' | ')})[]`;
default: return 'unknown';
}
};
const NULLABLE = new Set(['text', 'textArea', 'richEditorV2', 'number', 'relation', 'custom', 'repeater']);
const ALWAYS = new Set(['boolean', 'mediaList', 'select', 'relationList']);
const line = (f) => {
const opt = f.required || ALWAYS.has(f.kind) ? '' : '?';
const nul = opt && NULLABLE.has(f.kind) ? ' | null' : '';
return ` ${f.fieldId}${opt}: ${base(f)}${nul};`;
};
const out = [`import type { MicroCMSImage, MicroCMSContentId, MicroCMSDate } from 'microcms-js-sdk';`, ''];
for (const c of schema.customFields ?? []) {
out.push(`export type ${pascal(c.fieldId)} = {`, ` fieldId: ${JSON.stringify(c.fieldId)};`,
...c.fields.map(line), '};', '');
}
out.push(`export type ${typeName} = {`, ...schema.apiFields.map(line), '};');
console.log(out.join('\n'));設計上の判断は3つあります。
required: trueのフィールドは省略不可にし、それ以外は?を付ける。selectの選択肢は、スキーマのselectItems[].valueからリテラル型の和集合にする。relationは参照先APIのエンドポイント名を型名にし、idと日時の共通フィールドを交差させる。
required: true なら常に値があるかどうかは、フィールドごとのレスポンス形式のページに記載がありません。このスクリプトの判断なので、下書き状態のコンテンツを扱うなら実際のレスポンスで確かめます。
実行して出力を見る
ブログAPIを想定した、title body eyecatch publishedDate category author sections の7フィールドと、カスタムフィールド2つのスキーマJSONを用意し、Node.js v20.18.0で実行しました。入力は、エクスポート形式に沿って手で作ったものです。
node scripts/gen-types.mjs schema/blog.json Blog > types/blog.ts出力は次の形になりました。
export type HeadingBlock = {
fieldId: "headingBlock";
heading?: string | null;
};
export type BodyBlock = {
fieldId: "bodyBlock";
text?: string | null;
};
export type Blog = {
title: string;
body?: string | null;
eyecatch?: MicroCMSImage;
publishedDate?: string;
category: ("ニュース" | "ブログ")[];
author?: Authors & MicroCMSContentId & MicroCMSDate | null;
sections?: (HeadingBlock | BodyBlock)[] | null;
};先頭には microcms-js-sdk から型を読み込む import type の行が付きます。Authors は参照先APIの型で、authors APIのスキーマも同じスクリプトに通して作ります。型名を決める引数が、そのまま参照名になる仕組みです。
型を使って一覧を取得する
型ができたら、SDKの getList にジェネリクスとして渡します。SDKのREADMEによると、レスポンスは contents totalCount limit offset を持つ形で、contents の各要素には設定したスキーマの型が入ります。
// app/page.tsx
import { client } from '@/libs/client';
import type { Blog } from '@/types/blog';
export default async function Home() {
const { contents } = await client.getList<Blog>({
endpoint: 'blogs',
queries: { limit: 10 },
});
return (
<ul>
{contents.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}id は型に書いていませんが、SDKが各要素に MicroCMSContentId と MicroCMSDate を交差させるため、post.id はそのまま使えます。
Getting Startedには、開発モードでは変更が即時に反映されますが、サーバーコンポーネントの既定の挙動では、fetchキャッシュを消して再ビルドしない限り内容が変わらないという注意があります。本番で更新が反映されないときは、先にこのキャッシュの扱いを疑います。
型を検証させる反復
型を作ったら、Claude Codeに検証まで回させます。指示は、実行するコマンドと、見るべき出力をセットで渡します。
schema/blog.json から types/blog.ts を作り直して、
npx tsc --noEmit の出力を読んで型エラーを全部直して。
フィールド名は schema の fieldId を正とする。tsc --noEmit のエラーは、フィールド名の打ち間違いや、省略可能な値の未処理をそのまま指摘します。たとえば post.body.length と書けば、body が string | null で省略可能なので、strictNullChecks が有効ならコンパイルが止まります。ここで直させるのが、型を先に作る理由です。
繰り返し使う指示は、CLAUDE.md に置いておくと毎回書かずに済みます。
## microCMS
- 型は schema/*.json から scripts/gen-types.mjs で生成する。types/ 配下は手で編集しない。
- スキーマを変えたら生成し直し、`npx tsc --noEmit` が通ることを確認する。
- APIキーは .env.local にだけ置く。読み取りは settings.json で止めてある。
- microCMSの取得は libs/client.ts 経由、サーバーコンポーネントからだけ呼ぶ。microCMS MCPをつなぐ場合の権限
スキーマの取得やコンテンツの下書き登録をClaudeに任せたいなら、microCMS MCPを使う手があります。microCMSがホスティングするリモートMCPサーバーで、ベータ版として提供されています。Claude Codeには次のコマンドで追加します。
claude mcp add --transport http microcms \
https://mcp.microcms.io/mcp/${MICROCMS_SERVICE_ID} \
--header "Authorization: Bearer ${MICROCMS_API_KEY}"ドキュメントの注意事項は3点です。
- 認証はAPIキーによるBearer認証で、OAuthには未対応。
- 実行できる操作は、設定したAPIキーの権限に従う。
- リクエストはmicroCMS側のサーバーから送られ、送信元IPは固定されない。APIのIPアドレス制限を設定しているサービスでは使えないことがある。
権限の絞り方として、ドキュメントは2層を挙げています。本番で使うキーとは別に、AI操作専用のAPIキーを作り、必要な権限だけを付けます。そのうえで、MCPクライアント側でも使えるツールを必要なものだけに絞ります。取得だけをさせるなら、作成・更新・削除のツールは無効にします。
Claude Codeでは、ツール名が mcp__<サーバー名>__<ツール名> の形になります。上のコマンドでサーバー名を microcms にしたなら、削除と公開作成は次のように止められます。
{
"permissions": {
"deny": [
"mcp__microcms__microcms_delete_content",
"mcp__microcms__microcms_delete_media",
"mcp__microcms__microcms_create_content_published"
]
}
}下書きだけを作らせる運用なら、microcms_create_content_draft を許可して、公開用のツールを止める形が組めます。
キーを設定ファイルに書かない
.mcp.json に書くと、チームで共有できます。headers と url では環境変数を ${VAR} の形で展開できるので、キーそのものはファイルに残りません。
{
"mcpServers": {
"microcms": {
"type": "http",
"url": "https://mcp.microcms.io/mcp/${MICROCMS_SERVICE_ID}",
"headers": {
"Authorization": "Bearer ${MICROCMS_API_KEY}"
}
}
}
}未設定の変数に既定値がないと、設定は読み込まれても ${VAR} がそのまま使われます。claude mcp list に警告が出るので、つながらないときは最初にここを見ます。
ANTHROPIC_API_KEY のようにClaude Code自身やクラウド事業者の認証情報にあたる名前は、リモートサーバー向けの url と headers では空として扱われます。MICROCMS_API_KEY のような独自の名前なら、そのまま展開されます。
つなぎ込みを確かめるには、claude mcp get microcms を実行します。短寿命のトークンで認証ヘッダーを作りたい場合は、headersHelperの設定も選べます。
つまずきやすい点
症状から確かめる場所
型エラーが出ないのに実行時に .map が落ちる
該当フィールドの
kindがselectかrepeaterかを、スキーマJSONで確かめます。型を単数で書いていないかも見ます。画像のない記事だけ表示が崩れる
eyecatchが?付きで生成されているか、types/blog.tsを開いて見ます。付いていなければ、スキーマのrequiredがtrueのままです。開発では見えるのに本番で古い
next build後のfetchキャッシュが残っていないか、本番のビルド履歴から見ます。deny したファイルの中身が出力に出た
出力したのが
grep -rかスクリプトかを、実行履歴で確かめます。どちらでもなければ、permissions.denyのパスの綴りを見直します。MCP がつながらない
claude mcp listの警告、キーの権限、APIのIPアドレス制限の順に見ます。
CMSの型は、スキーマを変えるたびに古くなります。生成スクリプトを package.json のscriptsに登録し、スキーマ変更のたびにClaude Codeへ「生成して tsc を通して」と頼む運用にすると、手で直す箇所が残りません。
同じ「CMSをClaude Codeで触る」でも、WordPressやWebflowへ静的なLPを反映する流れはClaude CodeでLPをCMSに実装するで扱っています。ヘッドレスCMSのSanityをConnector経由でつなぐ場合は、ClaudeとSanityを連携する方法が参考になります。