Claude Haiku の Batch API で技術ブログ記事の SEO メタデータを一括生成する実装ガイド
Claude 3.5 Haiku の Batch API を活用し、技術ブログの SEO メタデータ(タイトル・ディスクリプション・OGP)を一括生成する実装方法を解説。コストを90%削減しながら高品質な SEO 最適化を実現します。
技術ブログを運営していると、記事の SEO メタデータ(タイトル・ディスクリプション・OGP タグ)の作成に多くの時間を取られます。特に大量の記事を抱えている場合、一つ一つ手作業で最適化するのは非現実的です。
Claude 3.5 Haiku の Batch API を活用すれば、コストを通常の API 呼び出しの 50% に削減しながら、数百件の記事のメタデータを一括生成できます。この記事では、実際のコード例とともに実装方法を詳しく解説します。
Batch API とは?低コストで大量処理を実現する仕組み
Anthropic の Batch API は、最大 24 時間の処理時間を許容することで、通常の API 料金の 50% でリクエストを処理できるサービスです。2024年11月に正式リリースされ、2026年6月現在も多くの開発者に利用されています。
Batch API の特徴
- コスト削減: 通常の API 料金の 50% で利用可能
- 大量処理: 1 バッチあたり最大 10,000 リクエストまで処理可能
- 非同期処理: 結果は最大 24 時間以内に返される
- Claude 3.5 Haiku 対応: 高速・低コストモデルと組み合わせて最大のコスト効率を実現
通常 API との料金比較(Claude 3.5 Haiku)
| 処理方法 | 入力料金(1M tokens) | 出力料金(1M tokens) |
|---|---|---|
| 通常 API | $1.00 | $5.00 |
| Batch API | $0.50 | $2.50 |
技術ブログ 100 記事のメタデータ生成(1記事あたり入力2,000 tokens、出力500 tokens と仮定)の場合、通常 API で $0.45、Batch API で $0.225 となり、約 50% のコスト削減が実現できます。
Batch API の処理フロー
Batch API は以下のステップで動作します。
sequenceDiagram
participant Client as クライアント
participant API as Batch API
participant Worker as Claude Worker
Client->>API: 1. バッチリクエスト送信<br/>(JSON Lines形式)
API->>Client: 2. batch_id 返却
API->>Worker: 3. バックグラウンド処理開始
Worker->>Worker: 4. 各リクエストを順次処理
Worker->>API: 5. 処理完了・結果保存
Client->>API: 6. ステータス確認 (polling)
API->>Client: 7. 完了通知
Client->>API: 8. 結果ダウンロード
API->>Client: 9. JSON Lines形式の結果
Batch API の処理フローは非同期で実行され、クライアントは定期的にステータスをポーリングして完了を確認します。
実装手順 1: バッチリクエストの作成
Batch API では、リクエストを JSON Lines 形式(.jsonl)で送信します。各行が個別の API リクエストを表します。
TypeScript での実装例
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs/promises';
interface BlogPost {
id: string;
title: string;
content: string;
}
interface BatchRequest {
custom_id: string;
params: {
model: string;
max_tokens: number;
messages: Array<{
role: 'user' | 'assistant';
content: string;
}>;
};
}
// SEO メタデータ生成用のプロンプト
function createSEOPrompt(post: BlogPost): string {
return `以下のブログ記事に対して、SEO 最適化されたメタデータを生成してください。
記事タイトル: ${post.title}
記事内容(最初の1000文字):
${post.content.substring(0, 1000)}
以下の形式で JSON を出力してください:
{
"seo_title": "SEO最適化されたタイトル(60文字以内、主要キーワードを含む)",
"meta_description": "メタディスクリプション(120〜160文字、キーワードを自然に含む)",
"og_title": "OGPタイトル(60文字以内)",
"og_description": "OGPディスクリプション(100〜120文字)",
"keywords": ["キーワード1", "キーワード2", "キーワード3"]
}`;
}
// バッチリクエストファイルの生成
async function createBatchRequestFile(
posts: BlogPost[],
outputPath: string
): Promise<void> {
const requests: BatchRequest[] = posts.map((post) => ({
custom_id: post.id,
params: {
model: 'claude-3-5-haiku-20241022',
max_tokens: 1024,
messages: [
{
role: 'user',
content: createSEOPrompt(post),
},
],
},
}));
// JSON Lines 形式で書き出し(各行が1つの JSON オブジェクト)
const jsonlContent = requests.map((req) => JSON.stringify(req)).join('\n');
await fs.writeFile(outputPath, jsonlContent, 'utf-8');
console.log(`✅ バッチリクエストファイルを作成しました: ${outputPath}`);
console.log(`📊 リクエスト数: ${requests.length}`);
}
このコードは、ブログ記事の配列を受け取り、各記事に対する SEO メタデータ生成リクエストを JSON Lines 形式で生成します。
実装手順 2: バッチジョブの送信とステータス確認
作成した .jsonl ファイルを Batch API に送信し、処理の完了を待ちます。
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// バッチジョブの送信
async function submitBatchJob(requestFilePath: string): Promise<string> {
const batchFile = await fs.readFile(requestFilePath, 'utf-8');
const batch = await client.messages.batches.create({
requests: batchFile.split('\n').map((line) => JSON.parse(line)),
});
console.log(`🚀 バッチジョブを送信しました`);
console.log(`📋 Batch ID: ${batch.id}`);
console.log(`📊 処理状況: ${batch.processing_status}`);
return batch.id;
}
// ステータスのポーリング(5分ごとに確認)
async function waitForBatchCompletion(batchId: string): Promise<void> {
const POLL_INTERVAL = 5 * 60 * 1000; // 5分
const MAX_WAIT = 24 * 60 * 60 * 1000; // 24時間
const startTime = Date.now();
while (Date.now() - startTime < MAX_WAIT) {
const batch = await client.messages.batches.retrieve(batchId);
console.log(`⏳ ステータス: ${batch.processing_status}`);
console.log(`📈 進捗: ${batch.request_counts.succeeded}/${batch.request_counts.processing + batch.request_counts.succeeded + batch.request_counts.errored} 完了`);
if (batch.processing_status === 'ended') {
console.log('✅ バッチ処理が完了しました');
return;
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
}
throw new Error('⚠️ バッチ処理がタイムアウトしました');
}
Batch API は非同期で処理されるため、定期的にステータスを確認するポーリング処理が必要です。上記のコードでは5分ごとに確認していますが、プロダクション環境では Webhook を活用することも可能です。
実装手順 3: 結果の取得とパース
バッチ処理が完了したら、結果をダウンロードして各記事のメタデータを抽出します。
interface SEOMetadata {
seo_title: string;
meta_description: string;
og_title: string;
og_description: string;
keywords: string[];
}
interface BatchResult {
custom_id: string;
result: {
type: 'succeeded' | 'errored';
message?: {
content: Array<{ type: string; text: string }>;
};
error?: {
type: string;
message: string;
};
};
}
// 結果のダウンロードとパース
async function downloadAndParseBatchResults(
batchId: string
): Promise<Map<string, SEOMetadata>> {
const results = await client.messages.batches.results(batchId);
const metadataMap = new Map<string, SEOMetadata>();
for await (const result of results) {
const batchResult = result as BatchResult;
if (batchResult.result.type === 'succeeded' && batchResult.result.message) {
try {
const content = batchResult.result.message.content[0];
if (content.type === 'text') {
const metadata = JSON.parse(content.text) as SEOMetadata;
metadataMap.set(batchResult.custom_id, metadata);
}
} catch (error) {
console.error(`❌ パースエラー (ID: ${batchResult.custom_id}):`, error);
}
} else if (batchResult.result.type === 'errored') {
console.error(`❌ API エラー (ID: ${batchResult.custom_id}):`, batchResult.result.error);
}
}
console.log(`✅ ${metadataMap.size} 件のメタデータを取得しました`);
return metadataMap;
}
// Markdown ファイルの frontmatter を更新
async function updateFrontmatter(
postId: string,
metadata: SEOMetadata,
filePath: string
): Promise<void> {
const content = await fs.readFile(filePath, 'utf-8');
// frontmatter の更新(簡易実装)
const updatedContent = content.replace(
/^---\n([\s\S]*?)\n---/,
(match, yaml) => {
return `---
${yaml}
seo_title: "${metadata.seo_title}"
meta_description: "${metadata.meta_description}"
og_title: "${metadata.og_title}"
og_description: "${metadata.og_description}"
keywords: ${JSON.stringify(metadata.keywords)}
---`;
}
);
await fs.writeFile(filePath, updatedContent, 'utf-8');
console.log(`✅ 更新完了: ${filePath}`);
}
このコードは、Batch API の結果をストリーミングで取得し、各記事の SEO メタデータを抽出します。エラーハンドリングも含まれており、一部のリクエストが失敗しても他の結果を正常に処理できます。
全体のワークフロー
これまでのコードを組み合わせた完全な実装例です。
async function generateSEOMetadataForBlog(postsDir: string) {
// 1. ブログ記事の読み込み
const postFiles = await fs.readdir(postsDir);
const posts: BlogPost[] = await Promise.all(
postFiles
.filter((file) => file.endsWith('.md'))
.map(async (file) => {
const content = await fs.readFile(`${postsDir}/${file}`, 'utf-8');
return {
id: file.replace('.md', ''),
title: extractTitle(content), // frontmatter からタイトル抽出
content: content,
};
})
);
console.log(`📚 ${posts.length} 件の記事を読み込みました`);
// 2. バッチリクエストファイルの作成
const requestFile = './batch_requests.jsonl';
await createBatchRequestFile(posts, requestFile);
// 3. バッチジョブの送信
const batchId = await submitBatchJob(requestFile);
// 4. 処理完了まで待機
await waitForBatchCompletion(batchId);
// 5. 結果の取得
const metadataMap = await downloadAndParseBatchResults(batchId);
// 6. Markdown ファイルの更新
for (const [postId, metadata] of metadataMap.entries()) {
const filePath = `${postsDir}/${postId}.md`;
await updateFrontmatter(postId, metadata, filePath);
}
console.log('🎉 すべての処理が完了しました');
}
// 実行
generateSEOMetadataForBlog('./content/posts');
実行結果とコスト試算
実際に 100 件の技術ブログ記事に対して実行した場合の例:
📚 100 件の記事を読み込みました
✅ バッチリクエストファイルを作成しました: ./batch_requests.jsonl
📊 リクエスト数: 100
🚀 バッチジョブを送信しました
📋 Batch ID: batch_abc123xyz
⏳ ステータス: in_progress
📈 進捗: 45/100 完了
⏳ ステータス: in_progress
📈 進捗: 89/100 完了
✅ バッチ処理が完了しました
✅ 98 件のメタデータを取得しました
✅ 更新完了: ./content/posts/article-001.md
...
🎉 すべての処理が完了しました
コスト試算(Claude 3.5 Haiku Batch API):
- 入力: 100 記事 × 2,000 tokens = 200,000 tokens = $0.10
- 出力: 100 記事 × 500 tokens = 50,000 tokens = $0.125
- 合計: $0.225
同じ処理を通常 API で実行した場合は $0.45 かかるため、50% のコスト削減を実現しています。
エラーハンドリングとリトライ戦略
Batch API では一部のリクエストが失敗する可能性があります。失敗したリクエストのみを再送信する仕組みを実装しましょう。
async function retryFailedRequests(
batchId: string,
originalPosts: BlogPost[]
): Promise<void> {
const results = await client.messages.batches.results(batchId);
const failedIds: string[] = [];
for await (const result of results) {
const batchResult = result as BatchResult;
if (batchResult.result.type === 'errored') {
failedIds.push(batchResult.custom_id);
}
}
if (failedIds.length === 0) {
console.log('✅ すべてのリクエストが成功しました');
return;
}
console.log(`⚠️ ${failedIds.length} 件のリクエストが失敗しました。再試行します...`);
const failedPosts = originalPosts.filter((post) =>
failedIds.includes(post.id)
);
const retryFile = './batch_requests_retry.jsonl';
await createBatchRequestFile(failedPosts, retryFile);
const retryBatchId = await submitBatchJob(retryFile);
await waitForBatchCompletion(retryBatchId);
console.log('✅ リトライが完了しました');
}
パフォーマンスとベストプラクティス
1. リクエストサイズの最適化
Batch API では各リクエストのサイズに上限があります(最大 32,768 tokens)。長文記事の場合は、記事の最初の部分のみを送信するようにしましょう。
function createSEOPrompt(post: BlogPost): string {
const MAX_CONTENT_LENGTH = 2000; // 約1,500 tokens
const truncatedContent = post.content.substring(0, MAX_CONTENT_LENGTH);
return `以下のブログ記事に対して、SEO 最適化されたメタデータを生成してください。
記事タイトル: ${post.title}
記事内容(抜粋):
${truncatedContent}
${post.content.length > MAX_CONTENT_LENGTH ? '...' : ''}
...(プロンプトの続き)`;
}
2. バッチサイズの調整
1 バッチあたり最大 10,000 リクエストまで送信できますが、実用上は 100〜500 件ごとに分割することをおすすめします。これにより、エラー時の影響範囲を限定できます。
function chunkArray<T>(array: T[], chunkSize: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
async function processBlogsInBatches(posts: BlogPost[]) {
const BATCH_SIZE = 200;
const chunks = chunkArray(posts, BATCH_SIZE);
for (let i = 0; i < chunks.length; i++) {
console.log(`📦 バッチ ${i + 1}/${chunks.length} を処理中...`);
await generateSEOMetadataForBlog(chunks[i]);
}
}
3. キャッシュの活用
同じ記事に対して複数回メタデータを生成する場合、結果をキャッシュしておくことでコストを削減できます。
import crypto from 'crypto';
function generateCacheKey(post: BlogPost): string {
const hash = crypto.createHash('sha256');
hash.update(post.title + post.content);
return hash.digest('hex');
}
async function getCachedMetadata(
cacheKey: string
): Promise<SEOMetadata | null> {
try {
const cached = await fs.readFile(`./cache/${cacheKey}.json`, 'utf-8');
return JSON.parse(cached);
} catch {
return null;
}
}
async function setCachedMetadata(
cacheKey: string,
metadata: SEOMetadata
): Promise<void> {
await fs.mkdir('./cache', { recursive: true });
await fs.writeFile(
`./cache/${cacheKey}.json`,
JSON.stringify(metadata),
'utf-8'
);
}
まとめ
Claude 3.5 Haiku の Batch API を活用することで、技術ブログの SEO メタデータを低コストかつ効率的に一括生成できます。
この記事で学んだこと:
- Batch API は通常の API 料金の 50% で大量処理が可能
- JSON Lines 形式でリクエストを送信し、非同期で処理を待つ
- 100 記事のメタデータ生成が約 $0.225 で実現できる
- エラーハンドリングとリトライ戦略で信頼性を向上
- キャッシュやバッチサイズの調整でさらなる最適化が可能
このワークフローを活用すれば、数百〜数千件の記事を抱えるブログでも、手作業では不可能だった大規模な SEO 最適化が自動化できます。ぜひ実装して、検索エンジンからの流入増加を実現してください。