ai-sdk-ui について
ai-sdk-uiは、Vercel AI SDK v5でReact UIを構築するためのリファレンススキルです。チャット、補完、構造化オブジェクトのフックに加え、ストリーミング、Next.jsの実装例、移行、トラブルシューティングを扱います。
主な機能
- 01useChat、useCompletion、useObjectの解説
- 02チャットと補完のストリーミングパターン
- 03ツール呼び出し表示とファイル添付
- 04チャット履歴とメッセージ状態の管理
- 05Next.js App RouterとPages Routerの例
- 06v4からv5への移行と問題解決
活用シーン
- 01ストリーミング対応のReactチャットUI構築
- 02UIへのAI補完や構造化出力の追加
- 03チャットでのツール呼び出し表示とファイル添付
- 04AI SDK v4からv5へのUIフック移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill ai-sdk-uiインストール後、Claude Code で/ai-sdk-uiを入力して読み込まれたか確認します。
同梱ファイルREADME.mdSKILL.json
詳細
AI SDK UI - フロントエンド React フック
Vercel AI SDK v5 を使用した、AI を活用するユーザーインターフェース向けのフロントエンド React フックです。
バージョン:AI SDK v5.0.76+(安定版) フレームワーク:React 18+、Next.js 14+ 最終更新日:2025-10-22
クイックスタート(5 分)
インストール
npm install ai @ai-sdk/openai
基本的なチャットコンポーネント(v5)
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Chat() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
/>
</form>
</div>
);
}
API ルート(Next.js App Router)
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toDataStreamResponse();
}
結果:フロントエンドのコード約 10 行で、AI の応答をストリーミングする機能的なチャットインターフェースを作成できます。
useChat フック - 完全リファレンス
基本的な使い方(v5 のパターン)
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatComponent() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.map(message => (
<div
key={message.id}
className={message.role === 'user' ? 'text-right' : 'text-left'}
>
<div className="inline-block p-2 rounded bg-gray-100">
{message.content}
</div>
</div>
))}
{isLoading && <div className="text-gray-500">AI is thinking...</div>}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="w-full p-2 border rounded"
/>
</form>
{/* Error */}
{error && <div className="text-red-500 p-4">{error.message}</div>}
</div>
);
}
API リファレンス全体
const {
// Messages
messages, // Message[] - Chat history
setMessages, // (messages: Message[]) => void - Update messages
// Actions
sendMessage, // (message: { content: string }) => void - Send message (v5)
reload, // () => void - Reload last response
stop, // () => void - Stop current generation
// State
isLoading, // boolean - Is AI responding?
error, // Error | undefined - Error if any
// Data
data, // any[] - Custom data from stream
metadata, // object - Response metadata
} = useChat({
// Required
api: '/api/chat', // API endpoint
// Optional
id: 'chat-1', // Chat ID for persistence
initialMessages: [], // Initial messages (controlled mode)
// Callbacks
onFinish: (message, options) => {}, // Called when response completes
onError: (error) => {}, // Called on error
// Configuration
headers: {}, // Custom headers
body: {}, // Additional body data
credentials: 'same-origin', // Fetch credentials
// Streaming
streamProtocol: 'data', // 'data' | 'text' (default: 'data')
});
v4 → v5 の破壊的変更
重要:v5 では useChat が入力状態を管理しなくなりました!
v4(旧 - 使用しないでください):
const { messages, input, handleInputChange, handleSubmit, append } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
v5(新 - 正しい方法):
const { messages, sendMessage } = useChat();
const [input, setInput] = useState('');
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
v5 の変更点の概要:
- 入力管理を削除:
input、handleInputChange、handleSubmitはなくなりました append()→sendMessage():メッセージ送信用の新しいメソッドですonResponseを削除:代わりにonFinishを使用してくださいinitialMessages→ 制御モード:完全に制御するにはmessagesプロパティを使用してくださいmaxStepsを削除:サーバー側でのみ処理してください
移行ガイドの全容については、references/use-chat-migration.md を参照してください。
UI でのツール呼び出し
API でツールを使用すると、useChat がメッセージストリーム内のツール呼び出しを自動的に処理します。
'use client';
import { useChat } from 'ai/react';
export default function ChatWithTools() {
const { messages } = useChat({ api: '/api/chat' });
return (
<div>
{messages.map(message => (
<div key={message.id}>
{/* Text content */}
{message.content && <p>{message.content}</p>}
{/* Tool invocations */}
{message.toolInvocations?.map((tool, idx) => (
<div key={idx} className="bg-blue-50 p-2 rounded my-2">
<div className="font-bold">Tool: {tool.toolName}</div>
<div className="text-sm">
<strong>Args:</strong> {JSON.stringify(tool.args, null, 2)}
</div>
{tool.result && (
<div className="text-sm">
<strong>Result:</strong> {JSON.stringify(tool.result, null, 2)}
</div>
)}
</div>
))}
</div>
))}
</div>
);
}
ファイル添付
メッセージと一緒にファイル(画像、PDF など)をアップロードします。
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatWithAttachments() {
const { messages, sendMessage, isLoading } = useChat({ api: '/api/chat' });
const [input, setInput] = useState('');
const [files, setFiles] = useState<FileList | null>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({
content: input,
experimental_attachments: files
? Array.from(files).map(file => ({
name: file.name,
contentType: file.type,
url: URL.createObjectURL(file),
}))
: undefined,
});
setInput('');
setFiles(null);
};
return (
<div>
{/* Messages */}
{messages.map(m => (
<div key={m.id}>
{m.content}
{m.experimental_attachments?.map((att, idx) => (
<div key={idx}>
<img src={att.url} alt={att.name} />
</div>
))}
</div>
))}
{/* Input */}
<form onSubmit={handleSubmit}>
<input
type="file"
multiple
onChange={(e) => setFiles(e.target.files)}
accept="image/*"
/>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
メッセージの永続化
チャット履歴を localStorage に保存、読み込みします。
'use client';
import { useChat } from 'ai/react';
import { useEffect } from 'react';
export default function PersistentChat() {
const chatId = 'my-chat-1';
const { messages, setMessages, sendMessage } = useChat({
api: '/api/chat',
id: chatId,
initialMessages: loadMessages(chatId),
});
// Save messages whenever they change
useEffect(() => {
saveMessages(chatId, messages);
}, [messages, chatId]);
return (
<div>
{messages.map(m => (
<div key={m.id}>{m.role}: {m.content}</div>
))}
{/* Input form... */}
</div>
);
}
// Helper functions
function loadMessages(chatId: string) {
const stored = localStorage.getItem(`chat-${chatId}`);
return stored ? JSON.parse(stored) : [];
}
function saveMessages(chatId: string, messages: any[]) {
localStorage.setItem(`chat-${chatId}`, JSON.stringify(messages));
}
useCompletion フック - 完全リファレンス
基本的な使い方
'use client';
import { useCompletion } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Completion() {
const { completion, complete, isLoading, error } = useCompletion({
api: '/api/completion',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
complete(input);
setInput('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter a prompt..."
rows={4}
className="w-full p-2 border rounded"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Generating...' : 'Generate'}
</button>
</form>
{completion && (
<div className="mt-4 p-4 bg-gray-50 rounded">
<h3>Result:</h3>
<p>{completion}</p>
</div>
)}
{error && <div className="text-red-500">{error.message}</div>}
</div>
);
}
API リファレンス全体
const {
completion, // string - Current completion text
complete, // (prompt: string) => void - Trigger completion
setCompletion, // (completion: string) => void - Update completion
isLoading, // boolean - Is generating?
error, // Error | undefined - Error if any
stop, // () => void - Stop generation
} = useCompletion({
api: '/api/completion',
id: 'completion-1',
// Callbacks
onFinish: (prompt, completion) => {},
onError: (error) => {},
// Configuration
headers: {},
body: {},
});
useCompletion 用 API ルート
// app/api/completion/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: openai('gpt-3.5-turbo'),
prompt,
maxOutputTokens: 500,
});
return result.toDataStreamResponse();
}
useObject フック - 完全リファレンス
基本的な使い方
構造化データ(フォーム、JSON オブジェクトなど)をライブ更新しながらストリーミングします。
'use client';
import { useObject } from 'ai/react';
import { z } from 'zod';
const recipeSchema = z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.string()),
instructions: z.array(z.string()),
}),
});
export default function RecipeGenerator() {
const { object, submit, isLoading, error } = useObject({
api: '/api/recipe',
schema: recipeSchema,
});
return (
<div>
<button onClick={() => submit('pasta carbonara')} disabled={isLoading}>
Generate Recipe
</button>
{isLoading && <div>Generating recipe...</div>}
{object?.recipe && (
<div className="mt-4">
<h2 className="text-2xl font-bold">{object.recipe.name}</h2>
<h3 className="text-xl mt-4">Ingredients:</h3>
<ul>
{object.recipe.ingredients?.map((ingredient, idx) => (
<li key={idx}>{ingredient}</li>
))}
</ul>
<h3 className="text-xl mt-4">Instructions:</h3>
<ol>
{object.recipe.instructions?.map((step, idx) => (
<li key={idx}>{step}</li>
))}
</ol>
</div>
)}
{error && <div className="text-red-500">{error.message}</div>}
</div>
);
}
API リファレンス全体
const {
object, // Partial<T> - Partial object (updates as stream progresses)
submit, // (input: string) => void - Trigger generation
isLoading, // boolean - Is generating?
error, // Error | undefined - Error if any
stop, // () => void - Stop generation
} = useObject({
api: '/api/object',
schema: zodSchema, // Zod schema
// Callbacks
onFinish: (object) => {},
onError: (error) => {},
});
useObject 用 API ルート
// app/api/recipe/route.ts
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamObject({
model: openai('gpt-4'),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.string()),
instructions: z.array(z.string()),
}),
}),
prompt: `Generate a recipe for ${prompt}`,
});
return result.toTextStreamResponse();
}
Next.js との統合
App Router の完全な例
ディレクトリ構成:
app/
├── api/
│ └── chat/
│ └── route.ts # Chat API endpoint
├── chat/
│ └── page.tsx # Chat page
└── layout.tsx
チャットページ:
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent, useRef, useEffect } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(message => (
<div
key={message.id}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-900'
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-200 p-3 rounded-lg">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce delay-200"></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border-t border-red-200 text-red-700">
Error: {error.message}
</div>
)}
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}
API ルート:
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
system: 'You are a helpful AI assistant.',
maxOutputTokens: 1000,
});
return result.toDataStreamResponse();
}
Pages Router の完全な例
ディレクトリ構成:
pages/
├── api/
│ └── chat.ts # Chat API endpoint
└── chat.tsx # Chat page
チャットページ:
// pages/chat.tsx
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatPage() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">AI Chat</h1>
<div className="border rounded p-4 h-96 overflow-y-auto mb-4">
{messages.map(m => (
<div key={m.id} className="mb-4">
<strong>{m.role === 'user' ? 'You' : 'AI'}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex space-x-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="flex-1 p-2 border rounded"
/>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Send
</button>
</form>
</div>
);
}
API ルート:
// pages/api/chat.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { messages } = req.body;
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
// Pages Router uses pipeDataStreamToResponse
return result.pipeDataStreamToResponse(res);
}
主な違い:App Router では toDataStreamResponse() を使用し、Pages Router では pipeDataStreamToResponse() を使用します。
UI でよくあるエラーと解決策
ドキュメント全体については、references/top-ui-errors.md を参照してください。クイックリファレンス:
1. useChat でストリームの解析に失敗する
エラー:SyntaxError: Unexpected token in JSON at position X
原因:API ルートが正しいストリーム形式を返していません。
解決策:
// ✅ CORRECT
return result.toDataStreamResponse();
// ❌ WRONG
return new Response(result.textStream);
2. useChat が応答しない
原因:API ルートが正しくストリーミングしていません。
解決策:
// App Router - use toDataStreamResponse()
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse(); // ✅
}
// Pages Router - use pipeDataStreamToResponse()
export default async function handler(req, res) {
const result = streamText({ /* ... */ });
return result.pipeDataStreamToResponse(res); // ✅
}
3. デプロイ後にストリーミングが機能しない
原因:デプロイプラットフォームが応答をバッファリングしています。
解決策:Vercel はストリーミングを自動検出します。他のプラットフォームでは設定が必要な場合があります。
4. useChat で body の値が古いままになる
原因:body オプションは最初のレンダリング時にのみ取得されます。
解決策:
// ❌ WRONG - body captured once
const { userId } = useUser();
const { messages } = useChat({
body: { userId }, // Stale!
});
// ✅ CORRECT - use controlled mode
const { userId } = useUser();
const { messages, sendMessage } = useChat();
sendMessage({
content: input,
data: { userId }, // Fresh on each send
});
5. React の最大更新深度エラー
原因:useEffect 内で無限ループが発生しています。
解決策:
// ❌ WRONG
useEffect(() => {
saveMessages(messages);
}, [messages, saveMessages]); // saveMessages triggers re-render!
// ✅ CORRECT
useEffect(() => {
saveMessages(messages);
}, [messages]); // Only depend on messages
よくあるエラーをさらに 7 つ紹介しています。references/top-ui-errors.md を参照してください。
ストリーミングのベストプラクティス
パフォーマンス
UX を向上させるため、必ずストリーミングを使用してください:
// ✅ GOOD - Streaming (shows tokens as they arrive)
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD - Non-streaming (user waits for full response)
const response = await fetch('/api/chat', { method: 'POST' });
UX パターン
読み込み状態を表示する:
{isLoading && <div>AI is typing...</div>}
停止ボタンを用意する:
{isLoading && <button onClick={stop}>Stop</button>}
最新のメッセージまで自動スクロールする:
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
読み込み中は入力を無効にする:
<input disabled={isLoading} />
ベストプラクティスの詳細については、references/streaming-patterns.md を参照してください。
このスキルを使用する場面
次の場合は ai-sdk-ui を使用してください:
- React のチャットインターフェースを構築する
- UI で AI による補完を実装する
- フロントエンドに AI の応答をストリーミングする
- Next.js の AI アプリケーションを構築する
- チャットメッセージの状態を処理する
- UI にツール呼び出しを表示する
- AI とのファイル添付を管理する
- v4 から v5 に移行する(UI フック)
- useChat/useCompletion のエラーが発生した
使用しない場合:
- バックエンドの AI 機能が必要な場合 → 代わりに ai-sdk-core を使用してください
- React 以外のフロントエンド(Svelte、Vue)を構築する場合 → 公式ドキュメントを確認してください
- Generative UI / RSC が必要な場合 → https://ai-sdk.dev/docs/ai-sdk-rsc を参照してください
- ネイティブアプリを構築する場合 → 別の SDK が必要です
関連スキル:
- ai-sdk-core - バックエンドでのテキスト生成、構造化出力、ツール、エージェント
- フルスタック AI アプリケーションには、両方を組み合わせてください
パッケージのバージョン
必須:
{
"dependencies": {
"ai": "^5.0.76",
"@ai-sdk/openai": "^2.0.53",
"react": "^18.2.0",
"zod": "^3.23.8"
}
}
Next.js:
{
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
バージョンに関する注記:
- AI SDK v5.0.76 以降(安定版)
- React 18 以降(React 19 に対応)
- Next.js 14 以降を推奨(13.4 以降も動作します)
- スキーマ検証には Zod 3.23.8 以降
公式ドキュメントへのリンク
コア UI フック:
- AI SDK UI の概要:https://ai-sdk.dev/docs/ai-sdk-ui/overview
- useChat:https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- useCompletion:https://ai-sdk.dev/docs/ai-sdk-ui/completion
- useObject:https://ai-sdk.dev/docs/ai-sdk-ui/object-generation
高度なトピック(リンクのみ):
- Generative UI(RSC):https://ai-sdk.dev/docs/ai-sdk-rsc/overview
- ストリームプロトコル:https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- メッセージのメタデータ:https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
Next.js との統合:
- Next.js App Router:https://ai-sdk.dev/docs/getting-started/nextjs-app-router
- Next.js Pages Router:https://ai-sdk.dev/docs/getting-started/nextjs-pages-router
移行とトラブルシューティング:
- v4→v5 の移行:https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- トラブルシューティング:https://ai-sdk.dev/docs/troubleshooting
- よくある問題:https://ai-sdk.dev/docs/troubleshooting/common-issues
Vercel へのデプロイ:
- Vercel Functions:https://vercel.com/docs/functions
- Vercel でのストリーミング:https://vercel.com/docs/functions/streaming
テンプレート
このスキルには、templates/ に次のテンプレートが含まれています。
- use-chat-basic.tsx - 手動入力による基本的なチャット(v5 パターン)
- use-chat-tools.tsx - ツール呼び出し UI を描画するチャット
- use-chat-attachments.tsx - ファイル添付のサポート
- use-completion-basic.tsx - 基本的なテキスト補完
- use-object-streaming.tsx - 構造化データのストリーミング
- nextjs-chat-app-router.tsx - Next.js App Router の完全な例
- nextjs-chat-pages-router.tsx - Next.js Pages Router の完全な例
- nextjs-api-route.ts - App Router と Pages Router の両方に対応した API ルート
- message-persistence.tsx - チャット履歴の保存と読み込み
- custom-message-renderer.tsx - Markdown に対応したカスタムメッセージコンポーネント
- package.json - 依存関係のテンプレート
リファレンスドキュメント
references/ を参照してください。
- use-chat-migration.md - v4→v5 の完全な移行ガイド
- streaming-patterns.md - UI ストリーミングのベストプラクティス
- top-ui-errors.md - よくある UI エラー 12 件と解決策
- nextjs-integration.md - Next.js のセットアップパターン
- links-to-official-docs.md - 公式ドキュメントへのリンク集
本番環境でのテスト済み:WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) 最終更新日:2025-10-22