Claude API + Cloudflare Workers で毎日の技術記事収集を自動化

背景

日々の技術情報のキャッチアップは欠かせません。Apple Developer News、Swift.org Blog、技術ブログなど、複数の情報源を毎日チェックします。

しかし、複数の情報源を毎日巡回し、記事を読んで要約を作成して共有する作業は、かなりの時間を取られていました。見逃しや確認漏れのリスクもあります。

そこで、Claude API と Cloudflare Workers を使って、情報収集から共有までを完全自動化することにしました。

なぜ Claude API + Cloudflare なのか

n8n や Zapier のようなワークフロー自動化ツールも選択肢でしたが、以下の理由で Claude API と Cloudflare Workers を選びました。

まず、Claude Sonnet 4.5 による要約の品質が非常に高いです。文脈を理解した上で、記事の要点を的確にまとめてくれます。

次に、TypeScript で自由に処理を拡張できる柔軟性があります。情報源の追加やフィルタリングロジックの変更が容易です。

そして、Cloudflare Workers の無料枠内で運用できるため、完全無料で使えます。Cloudflare Workflows によるステップごとのリトライ機構と、Dashboard での実行状況の可視化も便利です。

システム概要

情報源

毎日、以下の情報源から記事を収集しています。

RSS フィード(xml2js で解析)から取得するもの:

Web ページ(cheerio で解析)から取得するもの:

アーキテクチャ

技術記事収集フロー

毎日 10:00 JST (01:00 UTC) に Cloudflare Cron Trigger がワークフローを起動します。RSS/Web から記事を取得し、昨日以降の記事のみフィルタリング。Claude API で記事を一括要約し、Slack に投稿します。

Cron Trigger (毎日 01:00 UTC / 10:00 JST)
  ↓
Cloudflare Workflow
  ↓
  ├─ Step 1: fetch-articles (リトライ3回)
  │   └─ 7つの情報源から記事取得
  ↓
  ├─ Step 2: summarize-articles (リトライ2回)
  │   └─ Claude Sonnet 4.5 で一括要約
  ↓
  └─ Step 3: post-to-slack (リトライ3回)
      └─ Slack Webhook 投稿

Slack への投稿例

Slack 投稿例

情報源ごとにグループ化され、各記事のタイトル、URL、要約が自動投稿されます。

セットアップ手順

1. プロジェクトの初期化

npm create cloudflare@latest kusumoto-daily-report
cd kusumoto-daily-report
npm install @anthropic-ai/sdk cheerio xml2js
npm install -D @types/xml2js

2. 実装

プロジェクト構成に従って実装を行います。

src/
├── index.ts             # Cron エントリポイント
├── workflow.ts          # Workflow 定義
├── types.ts             # 型定義
├── steps/
│   ├── fetch-articles.ts
│   ├── summarize.ts
│   └── post-slack.ts
├── parsers/
│   ├── rss.ts
│   └── web.ts
├── clients/
│   ├── claude.ts
│   └── slack.ts
└── utils/
    └── date.ts

wrangler.toml に Cron Trigger と Workflow の設定を追加します。

name = "kusumoto-daily-report"
main = "src/index.ts"
compatibility_date = "2025-10-01"
compatibility_flags = ["nodejs_compat"]

[triggers]
crons = ["0 1 * * *"]

[[workflows]]
binding = "DAILY_REPORT_WORKFLOW"
name = "daily-report-workflow"
class_name = "DailyReportWorkflow"

3. Cloudflare にログイン

npx wrangler login

4. Secret の設定

Slack Webhook URL と Anthropic API Key を設定します。

npx wrangler secret put SLACK_WEBHOOK_URL
# プロンプトが表示されたら Slack Webhook URL を入力

npx wrangler secret put ANTHROPIC_API_KEY
# プロンプトが表示されたら Anthropic API Key を入力

5. デプロイ

npm run deploy

6. 動作確認

Cloudflare Dashboard で手動実行と実行状況を確認します。

ワークフロー画面

Cloudflare Workflow Dashboard

Workflows タブでは、過去 7 日間の実行履歴、完了したインスタンス数、エラー数などを確認できます。右上の「トリガー」ボタンから手動実行が可能です。

手動実行手順

  1. https://dash.cloudflare.com/ にアクセス
  2. Workers & Pages → kusumoto-daily-report を選択
  3. Workflows タブを開く
  4. 右上の「トリガー」ボタンをクリック

Workflow トリガーダイアログ

  1. インスタンス ID(任意)とパラメータ(任意)を入力
  2. 「トリガー インスタンス」ボタンで実行
  3. インスタンス一覧から実行状況を確認
    • 各ステップの成功/失敗
    • リトライ回数
    • 実行時間
    • エラーログ

技術詳細

プロジェクト構成

KusumotoDailyReport/
├── src/
│   ├── index.ts             # Cron エントリポイント
│   ├── workflow.ts          # Workflow 定義
│   ├── types.ts             # 型定義
│   ├── steps/
│   │   ├── fetch-articles.ts
│   │   ├── summarize.ts
│   │   └── post-slack.ts
│   ├── parsers/
│   │   ├── rss.ts           # RSS パーサー
│   │   └── web.ts           # Web スクレイパー
│   ├── clients/
│   │   ├── claude.ts        # Claude API クライアント
│   │   └── slack.ts         # Slack Webhook クライアント
│   └── utils/
│       └── date.ts          # 日付ユーティリティ
├── wrangler.toml            # Cloudflare 設定
├── tsconfig.json
└── package.json

記事取得の仕組み

情報源ごとに専用パーサーを実装しました。RSS は xml2js、Web ページは cheerio で解析します。

各情報源は独立して処理され、失敗時は最大 3 回試行します(初回 + リトライ 2 回、指数バックオフで 2 秒 → 4 秒)。リトライ後も失敗した情報源は、Slack に通知されます。

// 情報源ごとのリトライ処理
async function fetchFromSource(
  source: Source,
  targetDate: string,
  retries = 3
): Promise<Article[]> {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      let articles: Article[] = [];

      if (source.type === 'rss') {
        articles = await parseRSS(source.url, source.name, targetDate);
      } else {
        // Web スクレイピング
        if (source.url.includes('ios-osushi')) {
          articles = await parseIOSOsushi(targetDate);
        } else if (source.url.includes('xcode-cloud')) {
          articles = await parseXcodeCloud(targetDate);
        } else if (source.url.includes('swift.org')) {
          articles = await parseSwiftBlog(targetDate);
        }
      }

      console.log(`✓ ${source.name}: ${articles.length}件 (試行 ${attempt}/${retries})`);
      return articles;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      console.error(`✗ ${source.name}: ${errorMessage} (試行 ${attempt}/${retries})`);

      if (attempt === retries) {
        throw error;
      }

      // 指数バックオフでリトライ: 2s → 4s → 8s
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  return [];
}

// 全情報源から並列取得
export async function fetchArticles(sinceDate?: string): Promise<FetchSummary> {
  const targetDate = sinceDate || getYesterday();
  const allArticles: Article[] = [];
  const failedSources: Array<{ name: string; error: string }> = [];

  // 並列処理で全情報源から記事を取得
  const results = await Promise.allSettled(
    SOURCES.map((source) => fetchFromSource(source, targetDate, 3))
  );

  // 結果を集約
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      allArticles.push(...result.value);
    } else {
      const errorMessage =
        result.reason instanceof Error ? result.reason.message : String(result.reason);
      failedSources.push({
        name: SOURCES[index].name,
        error: errorMessage,
      });
    }
  });

  return {
    articles: allArticles,
    failedSources,
  };
}

失敗した情報源は、以下のように Slack に通知されます。

⚠️ *取得に失敗した情報源*

• *Yahoo! ニュース*
  エラー: Network timeout after 3 retries

• *Swift.org Blog*
  エラー: Failed to parse HTML structure

要約生成の仕組み(Claude API)

Claude API(Sonnet 4.5 モデル)を使って、全記事を一括で要約生成します。

export class ClaudeClient {
  private client: Anthropic;

  constructor(apiKey: string) {
    this.client = new Anthropic({ apiKey });
  }

  async summarizeArticles(articles: Article[]): Promise<ArticleWithSummary[]> {
    const prompt = this.buildPrompt(articles);

    const message = await this.client.messages.create({
      model: 'claude-sonnet-4-5-20250929',
      max_tokens: 4000,
      messages: [{ role: 'user', content: prompt }],
    });

    const responseText = message.content[0].type === 'text'
      ? message.content[0].text
      : '';
    const summaries = this.parseResponse(responseText);

    // 記事と要約をマージ
    return articles.map((article) => {
      const summary = summaries.find((s) => s.url === article.url);
      return {
        ...article,
        summary: summary?.summary || '要約を生成できませんでした',
      };
    });
  }

  private buildPrompt(articles: Article[]): string {
    const articlesJson = JSON.stringify(
      articles.map((a) => ({
        title: a.title,
        url: a.url,
        source: a.source,
      })),
      null,
      2
    );

    return `以下の記事リストを要約してください。各記事を200文字以内の日本語で要約し、JSON配列で出力してください。
英語の記事も含め、すべて日本語で要約してください。
要約のみを出力し、「要約:」などの前置きは不要です。

記事リスト:
${articlesJson}

出力形式:
[
  {"url": "記事のURL", "summary": "要約文"},
  ...
]`;
  }
}

Claude Sonnet 4.5 を使うことで、文脈を理解した高品質な要約が生成されます。全記事を 1 回の API 呼び出しでバッチ処理するため、コストも抑えられます。英語記事も含めすべて日本語で要約されるため、統一感のある出力が得られます。

Workflow の実装

Cloudflare Workflows でステップごとにリトライを制御しています。各情報源は独立して最大 3 回試行され、Step 1 全体の失敗時にも追加で最大 3 回試行します。

export class DailyReportWorkflow extends WorkflowEntrypoint<Env> {
  async run(event: unknown, step: WorkflowStep) {
    // Step 1: 記事取得
    // - 各情報源で最大3回試行(初回 + リトライ2回、指数バックオフ: 2s → 4s)
    // - Step 全体でも最大3回試行(初回 + リトライ2回、5秒間隔、指数バックオフ)
    const fetchResult: FetchSummary = await step.do(
      'fetch-articles',
      {
        retries: {
          limit: 3,
          delay: 5000,
          backoff: 'exponential',
        },
      },
      async () => {
        return await fetchArticles();
      }
    );

    const { articles, failedSources } = fetchResult;

    // 記事がある場合のみ要約を生成
    let summaries: ArticleWithSummary[] = [];
    if (articles.length > 0) {
      // Step 2: 要約生成(リトライ2回、10秒間隔)
      summaries = await step.do(
        'summarize-articles',
        {
          retries: {
            limit: 2,
            delay: 10000,
          },
        },
        async () => {
          return await summarizeArticles(articles, this.env.ANTHROPIC_API_KEY);
        }
      );
    }

    // Step 3: Slack 投稿(リトライ3回、5秒間隔)
    // 失敗した情報源も含めて通知
    await step.do(
      'post-to-slack',
      {
        retries: {
          limit: 3,
          delay: 5000,
        },
      },
      async () => {
        return await postToSlack(summaries, this.env.SLACK_WEBHOOK_URL, failedSources);
      }
    );
  }
}

GitHub との連携による自動デプロイ

Cloudflare Workers と GitHub を連携することで、コードの変更が自動的にデプロイされます。

PR でのデプロイ確認

Cloudflare Workers デプロイ通知

PR 内で Cloudflare によるデプロイ状況を確認できます。

運用とモニタリング

Cloudflare Dashboard での監視

Workflows タブで以下を確認できます。

  • 各ステップの実行状況(成功/失敗)
  • リトライ回数
  • 実行時間
  • エラーログ

ログ確認

ローカルでリアルタイムログを確認できます。

npx wrangler tail

コスト

完全無料で運用可能です。

  • Cloudflare Workers の無料枠(1 日 100,000 リクエスト)
  • Cloudflare Workflows の無料枠(Beta 期間中)
  • Cron Triggers の無料提供

合計で月額 $0 です。

まとめ

TypeScript、Claude API、Cloudflare Workers を組み合わせることで、技術記事の収集・要約・共有を完全自動化できました。

人手を介さず毎朝定時に情報収集が完了し、Cloudflare Dashboard で実行状況を可視化できます。設定さえ済ませてしまえば、あとは毎朝 Slack に要約が届くのを待つだけです。

以前は毎朝 30 分ほどかけて複数の情報源を巡回していましたが、今では自動化により、その時間を本来の業務に充てられるようになりました。Claude API の要約品質も高く、記事の要点を的確に把握できます。

同じような情報収集の課題を抱えている方は、ぜひ試してみてください。

参考リンク