はじめに
要約はできるようになりました。でも、このままだと問題があります。
ページを開くたびにRSSを読みに行き、ボタンを押すたびにGeminiで要約し直す。しかも、毎回ターミナルを開いて開発サーバーを起動しないと見られない。これでは「毎朝開くだけ」にはほど遠い状態です。
今回は、記事と要約をSupabaseに保存し、Vercelで公開して、毎朝自動でおすすめが作られるようにするところまでを書きます。
こんな方におすすめ
- Next.jsとSupabaseで、重複なくデータを保存したい方
- Vercelの無料プランで、毎日決まった時間に処理を動かしたい方
- 個人用のサイトに、手軽に鍵(アクセス制限)をかけたい方
- Chromebookから、GitHub経由でVercelにデプロイしたい方
「取得」と「表示」を分ける
最初に決めたのは、RSSを取得するタイミングと、ページを見るタイミングを分けることです。
- 取得:1日1回、RSSを読みに行って新しい記事だけ保存する
- 表示:ページを開いたら、保存済みのデータを表示するだけ
こうしておくと、ページを何度開いてもRSSやGeminiは使いません。要約もデータベースに保存しておけば、一度要約した記事で二度とAPIを使わずに済みます。
Supabaseのテーブル設計
記事は1つのテーブルにまとめました。あとで使う列も最初から用意しておくと、テーブルを作り直さずに済みます。
create table articles (
id bigint generated always as identity primary key,
link text not null unique, -- 同じURLは二重に入らない
title text not null,
snippet text not null default '',
source_name text not null,
bookmarks integer,
published_at timestamptz,
fetched_at timestamptz not null default now(),
summary text, -- AIの要約
keywords jsonb, -- キーワードと解説
reason text, -- おすすめ理由
recommended_on date -- どの日のおすすめか
);
alter table articles enable row level security;
ポイントは link を unique にしていることです。保存時に ignoreDuplicates を付けると、すでにある記事は自動でスキップされ、新しい記事だけが入ります。
await supabase
.from("articles")
.upsert(rows, { onConflict: "link", ignoreDuplicates: true });
また、行レベルセキュリティ(RLS)を有効にしてポリシーを作らないことで、サーバー側のシークレットキー以外からは読み書きできないようにしています。シークレットキーはブラウザに出ないよう、NEXT_PUBLIC_ を付けずにサーバー側だけで使います。
毎朝の処理を1つにまとめる
「記事を取得 → おすすめを選ぶ → 要約して保存」を1つの関数にまとめ、/api/daily から呼べるようにしました。
大事にしたのは、同じ日に何回実行しても結果が変わらないことです。その日のおすすめがすでにあれば、Geminiを使わずに終わります。自動実行が2回走っても、手動で押しても、無料枠を無駄にしません。
const { count } = await supabase
.from("articles")
.select("id", { count: "exact", head: true })
.eq("recommended_on", today);
if (count) return { message: "今日のおすすめは作成済みです" };
Vercelで毎朝自動実行する
VercelのCron機能を使うと、決まった時間にAPIを呼び出してくれます。設定は vercel.json に書くだけです。
{
"crons": [
{ "path": "/api/daily", "schedule": "0 21 * * *" }
]
}
注意点は2つあります。
- 時刻はUTCなので、
0 21 * * *は日本時間の朝6時 - 無料のHobbyプランは1日1回までで、実行時刻に最大1時間ほどのずれがある
今回は「朝開いたらできている」で十分なので、1日1回で問題ありませんでした。
また、/api/daily はインターネットに公開されるので、他人に叩かれるとGeminiの無料枠が減ってしまいます。CRON_SECRET という環境変数を登録しておくと、Vercelが自動実行時に合言葉を添えてくれるので、それを確認して弾くようにしています。
const secret = process.env.CRON_SECRET;
if (secret && request.headers.get("authorization") !== `Bearer ${secret}`) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
サイト全体に鍵をかける
個人用のサイトなので、IDとパスワードを知っている人だけが見られるようにしました。Next.jsの proxy.ts(バージョン15以前は middleware.ts)で、ベーシック認証をかけています。
export const config = {
// 毎朝の自動実行は CRON_SECRET で守っているので、鍵の対象から外す
matcher: ["/((?!api/daily|_next/static|_next/image|favicon.ico).*)"],
};
自動実行はIDとパスワードを入力できないので、/api/daily だけは鍵の対象から外しているのがポイントです。一度Chromeにパスワードを保存すれば、次からは自動で入れます。
Chromebookからデプロイする
GitHubへの送信には、GitHub公式のコマンドツール gh を使いました。ブラウザで承認するだけでログインでき、リポジトリの作成から送信までコマンドで済みます。
sudo apt install -y gh
gh auth login
gh repo create trend-study --private --source=. --remote=origin --push
あとはVercelでGitHubのリポジトリを読み込み、環境変数を登録すれば公開完了です。以降はGitHubに送るたびに、Vercelが自動でサイトを更新してくれます。
ハマったところ
鍵がかからない
公開したのに、サイトを開いてもパスワードを聞かれませんでした。確認すると、IDとパスワードの環境変数がVercel側にうまく連携されていなかったようです。Vercelの画面から設定し直し、再デプロイしたところ鍵がかかるようになりました。環境変数は、変更しただけでは今動いているサイトに反映されないので、変更後は再デプロイを忘れずに。
Gitの管理が始まっていなかった
create-next-app で作ったのに、なぜか git init されていませんでした。.gitignore に .env* が入っているかを git check-ignore .env.local で確認してから送ると安心です。
まとめ
- 「取得」と「表示」を分けて、ページを開くだけではAPIを使わないようにした
-
linkをuniqueにして、重複なく記事を保存した - 毎朝の処理は、同じ日に何回実行しても結果が変わらないようにした
- Vercel Cronは無料プランだと1日1回、時刻はUTCで指定する
- 自動実行は
CRON_SECRET、サイト全体はベーシック認証で守った
これで、朝ブックマークを開くだけで、今日のおすすめが要約付きで並ぶようになりました。次回は、カレンダー・トレンドワード・検索・総括を追加して、「読むだけで学べるサイト」にしていく話です。