作ったもの
毎朝Slackにテックニュースが届く仕組みを作りました。
- ユーザーが登録したRSSフィードから最新記事を自動取得
- AIが記事を読んで日本語で要約
- セキュリティ関連やbreaking changeは優先的に報告
- 1日の記事を総括した「今日のテックニュース総括」も生成
- アプリケーションで過去の記事を検索
なぜ作ったか
社内に技術ニュースを発信してくれる先輩がいました。Next.jsやTanStack Start、ConvexやHonoなどモダンな技術のこと、AIニュースの最新動向、ビジネススキルの発信、英語学習などなど。脚色無しで、1日に5〜10件のニュースを自身の考えと合わせて発信されていました。純粋にすごいです。
自分も同じように情報を取得し、発信したいと思いましたが、先輩と同じクオリティでやるのは正直難しい。毎日発表されるニュースを取得して、英語の記事を読んで、重要なものをピックアップして......。この部分だけでもAIに任せられたら楽になるなと思い、社内向けにAIニュースアプリを作りました。
技術スタック
| 用途 | 技術 |
|---|---|
| フレームワーク | Next.js (App Router) |
| AI | Vercel AI SDK |
| DB | Turso (SQLite) |
| ORM | Drizzle |
| 定期実行 | Vercel Cron |
なぜVercel AI SDKか
複数のAIプロバイダーを統一インターフェースで扱えます。モデルを変えるのも1行だけです。
const result = await generateObject({
model: "google/gemini-2.5-flash-lite", // ここを変えるだけ
prompt,
schema: summarySchema,
});
Vercel AI Gatewayは毎月5ドル分のクレジットが付与されます。
Yes! When you sign up for a Vercel account, you get $5 of credits every 30 days to try out any model from our model list. We don’t restrict access to premium models.
Note: After you make your first payment, you are considered a paid customer and will no longer receive the free credits.
Vercelアカウントを登録すると、30日ごとに5ドル分のクレジットがもらえます。モデルリストのどのモデルでも試せて、プレミアムモデルへのアクセス制限もありません。
注意: 最初の支払いをすると有料顧客扱いになり、無料クレジットは付与されなくなります。
開発時にかなり使い倒しましたが、1ドルも使わなかったので当面は無料で使えそうです。
モデルは Gemini を採用しました。2025年10月末時点の開発時に要約時の精度がちょうど良かったためです。Claude は気を利かせすぎて遅い、GPT は微妙に精度が悪かったです。現在だとまた最適なモデルを選択する必要がありますね。
なぜRSSか
最初はスクレイピングも考えましたが、精度がサイトによってバラバラで安定しませんでした。AIのリソースも使いすぎます。RSSならXML形式で構造化されているので、パースが安定します。
アーキテクチャ
Vercel Cron (毎日 10時頃)
│
▼
/api/cron
│
├─ 1. RSS取得 (rss-parser)
│
├─ 2. AI処理 (Vercel AI SDK)
│ - 記事ごとに要約・タグ生成
│ - 全体の総括文を生成
│
├─ 3. DB保存 (Turso)
│
└─ 4. Slack通知 (Webhook)
実装
1. RSSフィードの取得
rss-parserでフィードを取得します。直近7日分、各フィード最新10件に絞っています。同じニュースを何度も発信しないよう、既存のDBと照合して重複チェックも行います。
import Parser from "rss-parser";
type FeedItem = {
date: Date;
title: string;
url: string;
content: string;
};
const parser = new Parser();
const MAX_ITEMS = 10;
const DAYS_TO_FETCH = 7;
export async function fetchFeedItems(feedUrl: string): Promise<FeedItem[]> {
const feed = await parser.parseURL(feedUrl);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - DAYS_TO_FETCH);
return feed.items
.map((item) => ({
date: new Date(item.isoDate ?? item.pubDate ?? ""),
title: item.title ?? "",
url: item.link ?? "",
content: item.contentSnippet ?? "",
}))
.filter((item) => item.date >= cutoffDate)
.slice(0, MAX_ITEMS);
}
2. AIによる要約生成
Vercel AI SDKのgenerateObjectを使います。Zodスキーマを渡すと、AIがその形式で出力してくれます。
import { generateObject } from "ai";
import { z } from "zod";
const summarySchema = z.object({
summaries: z.array(
z.object({
id: z.string(),
title: z.string(),
summary: z.string(),
tags: z.array(z.string()),
keyPoint: z.string(),
})
),
});
type SummaryInput = Record<'id' | 'title' | 'content', string>;
export async function generateSummaries(items: SummaryInput[]) {
const prompt = items
.map((item) => `[${item.id}] ${item.title}\n${item.content}`)
.join("\n\n");
const { object } = await generateObject({
model: "google/gemini-2.5-flash-lite",
prompt: `以下の技術記事を要約してください。\n\n${prompt}`,
schema: summarySchema,
});
return object.summaries;
}
generateObjectの良いところ:
- 出力がスキーマに沿っていることが保証される
- プロンプトで「JSON形式で出力して」と頑張らなくていい
- TypeScriptの型が効く
3. プロンプト
実際に使っているプロンプトはこんな感じです。
const ARTICLE_SUMMARY_PROMPT = `
以下の技術記事群を分析し、それぞれについて以下を生成してください。
1. より分かりやすい日本語のタイトル
2. 5文程度の詳細な要約
3. 適切なタグを1〜3個
4. 記事の核心ポイントを1文で
利用可能なタグ:
- release: リリース情報
- security: セキュリティ関連
- breaking-change: 破壊的変更
- feature: 新機能
- bugfix: バグ修正
...
`;
タグは自由入力じゃなくて選択式にしています。後でフィルタリングしやすく、AIの出力がブレにくいためです。
4. 全体サマリー
記事ごとの要約とは別に、1日の総括も生成しています。
const OVERALL_SUMMARY_PROMPT = `
今日のテックニュースをまとめてください。
**最優先で報告すべき内容:**
- セキュリティ: 脆弱性、セキュリティアップデート
- 重大なバグ、breaking change
- メジャーバージョンリリース
上記がある場合は100-200文字で簡潔に。
ない場合は400-500文字で詳しく。
`;
セキュリティ関連は優先的に報告させています。重要な脆弱性情報が他のニュースに埋もれないようにするためです。
5. Slack通知
Slack Webhookで送信します。Block Kit形式でリッチに表示できます。
import type { KnownBlock } from "@slack/types";
export async function sendSlackMessage(blocks: KnownBlock[]) {
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error("SLACK_WEBHOOK_URL is not set");
}
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ blocks }),
});
}
Block Kitのメッセージ構造:
type ArticleBlockParams = {
url: string;
title: string;
summary: string;
keyPoint: string;
imageUrl?: string;
};
function createArticleBlock(params: ArticleBlockParams): KnownBlock {
const { url, title, summary, keyPoint, imageUrl } = params;
return {
type: "section",
text: {
type: "mrkdwn",
text: `*<${url}|${title}>*\n${summary}\n📍 *ポイント:* ${keyPoint}`,
},
...(imageUrl && {
accessory: {
type: "image",
image_url: imageUrl,
alt_text: title,
},
}),
};
}
6. 定期実行
vercel.jsonに書くだけです。UTC 01:00 = 日本時間 10:00 です。
{
"crons": [
{
"path": "/api/cron",
"schedule": "0 1 * * *"
}
]
}
7. AIによるRSSリンク検索
RSSのリンクを人間が探してくるのは面倒なので、AIに探させる機能も作りました。
const rssFeedSchema = z.object({
feeds: z.array(
z.object({
url: z.string().url(),
title: z.string(),
description: z.string(),
})
),
});
export async function searchRssFeeds(feedName: string) {
const { object } = await generateObject({
model: "google/gemini-2.5-pro",
prompt: `${feedName}の公式RSSフィードURLを探してください`,
schema: rssFeedSchema,
});
return object.feeds;
}
探したリンクが本当に有効なRSSかどうか、裏側で検証してから結果を返すようにしています。Gemini flash だと検証が甘かったので、ここだけGemini pro版を使用しています。
実際に取得されたURL:
https://github.com/vercel/next.js/releases.atom
運用してみて
2025年11月初旬に作り、約1ヶ月ほど運用しています。社内の反応は良かったです。
ただ、受け取る情報を設定しすぎると情報過多になってしまいます。Slackに発信する量を調整したり、アプリ側にページを設けてSlackにはリンクだけ載せるようにした方が良さそうです。
また、記事内のコードでは gemini-2.5-flash-lite や gemini-2.5-pro を使っていますが、AIモデルは日々進化しているので、その時点で最適なモデルを選ぶのが良いと思います。
まとめ
- Vercel AI SDKでAIの呼び出しが簡単
- 無料枠で十分動く
- Vercel Cronで定期実行も楽
AIで自分の代わりに情報収集させるのは、思ったより実用的でした。社内ツールなので気軽に試せますし、フィードバックももらいやすいです。
今後も改良を加えて、ニュースの質を高めていきたいです。





