メインコンテンツへスキップ
#AI活用 約8分で読めます

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 最適化が自動化できます。ぜひ実装して、検索エンジンからの流入増加を実現してください。

参考リンク

#Claude #Batch API #SEO #メタデータ生成 #コスト削減
シェア