Next.jsのサイトを毎朝自動でビルド・デプロイして「予約公開」を成立させる──Mac1台のlaunchd+Vercel CLIで、二重起動ロック・失敗をDesktopに通知・本番URLの200確認・直近3日の取りこぼし確認・IndexNow送信まで。当サイトが2026年8月28日から動かしている仕組み
当サイトは、記事のfrontmatterの日付が「UTCの今日」以下になった記事だけを公開する予約公開を2026年8月27日に導入し、翌日から毎朝9時10分にMac上のlaunchdでnpm test→npm run build→vercel --prod --yesを回している。個別ページはビルド時に公開済みの分だけ静的生成する作りで、日付が変わっただけでは出ない(再ビルドまで404)ため、毎朝1回ビルドし直す必要があるからだ。この記事は、そのシェルスクリプトの構成をそのまま説明する。二重起動を防ぐmkdirロック(3時間より古いロックは異常終了とみなして外す)、失敗時にDesktopへテキストファイルを置く通知、デプロイ後に本番URLをcurlで叩いて200を数える確認、直近3日分の記事も再確認する取りこぼし対策、当日公開分の200だけをIndexNowに送る仕組み、停止するときの3点確認。9月17日のログでは10本を09:13に公開・送信している。

目次
2026年9月18日・日本時間時点の情報です。 「記事を先の日付で仕込んでおいて、その日になったら自動で出したい」は、Next.jsの静的サイトで意外と面倒な要件で、当サイトは2026年8月27日に予約公開を入れてから、翌28日に「毎朝1回ビルドしてデプロイする」仕組みをMac1台のlaunchdで動かしている。この記事は、その仕組みを動かしているシェルスクリプトの構成を、当サイトの実際のコードと実行ログを材料に説明する。Vercelへのデプロイ前の一般的なチェックはチェックリストの記事、Vercel CLIが「gitでなく作業ツリーをそのまま上げる」点はこちらに。
3行まとめ
- なぜ毎朝ビルドするのか。 予約公開は「frontmatterの
dateがUTCの今日以下の記事だけを一覧・sitemap・個別ページに出す」だけの単純な実装(isPublished(date) = date <= today)。だが個別ページはビルド時にgenerateStaticParamsで公開済みの分だけ静的生成され(現在はdynamicParams = falseなので、ビルドに含まれなかった記事は再ビルドまで404のまま。8月28日にスクリプトを書いた時点のコメントでは「ISRのrevalidate 3600で、誰かが先に404を踏むとキャッシュが切れるまで最大1時間出ない」だった)、日付が変わっただけでは出ない。ビルドし直せばその日の分が静的生成されるので、毎朝1回ビルドしてデプロイする。Google側の「同じ日に大量に出す」ことへの懸念(scaled content abuse)を避けて小分けに出す運用も、この仕組みが支える。- スクリプトの骨格(bash・約150行)。 launchdの
StartCalendarIntervalで毎朝9:10に起動→mkdirでロック(前回が3時間より長く残っていれば異常終了とみなして外す。そうしないと「毎朝黙ってスキップし続ける」)→npm test(失敗しても公開は続行し警告だけ)→npm run build(失敗したらデプロイしない)→vercel --prod --yes→本番トップと当日公開分のURLをcurlで叩いて200を数える→直近3日分の記事も全部200か確認→当日分で200だったURLだけIndexNowに送る。- 人が気づけるようにする。 launchdから
osascriptの通知は出ないことがあるので、失敗時はDesktopに「⚠️サイト日次デプロイ失敗_日付.txt」を置く(内容・ログの場所・手で直すコマンドを書く)。停止するときはlaunchctl bootoutのexit 0確認→plistをLaunchAgents.disabled/へ移動→launchctl listで空を確認、の3点。9月17日のログでは「本日公開予定=10本」を09:10に開始し、09:13にdeploy OK、10本のIndexNow送信OKで終わっている。
1. 予約公開の実装(Next.js側)
src/lib/posts.tsの該当部分はこれだけである。
// 予約公開の基準日(UTC の YYYY-MM-DD)。
// date がこの日より後の記事は「まだ公開していない」として一覧・sitemap・個別ページから外す。
function publishCutoff(): string {
return new Date().toISOString().slice(0, 10);
}
export function isPublished(date: string): boolean {
return date <= publishCutoff();
}
一覧・sitemap・generateStaticParamsがこのisPublishedで絞る。基準がUTCなので、日本時間の朝9時に「今日」の記事が出るのはUTCの0時(日本時間9時)を過ぎてからで、当サイトの9:10というデプロイ時刻はここから決まっている。
2. 毎朝のスクリプト(launchd側)
launchdのplistはProgramArgumentsに/bin/bashと~/bin/ai_jitan_daily_deploy.sh、StartCalendarIntervalにHour 9 / Minute 10、WorkingDirectoryにリポジトリ、StandardOutPathとStandardErrorPathに~/logs/のファイルを指定している。スクリプトの流れを、実際のコードから抜粋する。
二重起動のロック(2026年9月4日改修)
LOCK="/tmp/ai_jitan_daily_deploy.lock"
# 前回が異常終了するとロックが残り、以後このジョブは毎朝黙ってスキップし続ける。
# 3時間より古いロックは「前回が死んだ」とみなして外す(正常な実行は数分で終わる)。
if [ -d "$LOCK" ]; then
LOCK_AGE=$(( $(date +%s) - $(stat -f %m "$LOCK") ))
if [ "$LOCK_AGE" -gt 10800 ]; then rmdir "$LOCK" 2>/dev/null; fi
fi
if ! mkdir "$LOCK" 2>/dev/null; then
alert "前回の実行がまだ終わっていないため、今日のデプロイを見送った"; exit 0
fi
trap 'rmdir "$LOCK" 2>/dev/null' EXIT
失敗をDesktopに置く通知
alert() {
local f="$HOME/Desktop/⚠️サイト日次デプロイ失敗_$(date '+%Y-%m-%d').txt"
{ echo "AI時短ラボ サイトの日次デプロイが失敗しました"; echo "内容: $*"
echo "手で直す場合: cd ~/projects/ai-jitan-hub && npm run build && vercel --prod --yes"; } > "$f"
osascript -e "display notification \"$*\" with title \"サイトの日次デプロイが失敗\"" 2>/dev/null
}
テスト→ビルド→デプロイ
TODAY_UTC=$(date -u '+%Y-%m-%d')
DUE=$(grep -l "^date: \"$TODAY_UTC\"" content/news/*.md | wc -l | tr -d ' ')
say "開始 / UTC=$TODAY_UTC / 本日公開予定=${DUE}本"
npm test >> "$LOG" 2>&1 || alert "npm test が失敗している。記事の公開自体は続行した"
npm run build >> "$LOG" 2>&1 || { alert "npm run build が失敗したため、今日の記事は公開されていない"; exit 1; }
vercel --prod --yes >> "$LOG" 2>&1 || { alert "ビルドは通ったが vercel へのデプロイが失敗した"; exit 1; }
本番を叩いて確認する(実行と報告の一致)
for f in $(grep -l "^date: \"$TODAY_UTC\"" content/news/*.md); do
s=$(basename "$f" .md)
c=$(curl -sL -o /dev/null -w '%{http_code}' "https://www.ai-jitan-hub.com/news/$s")
[ "$c" = "200" ] && OK=$((OK+1)) || { NG=$((NG+1)); say " ★未公開: $s ($c)"; }
done
# 直近3日分も再確認(約250本=約150秒)
for d in 1 2 3; do DAY=$(date -u -v-${d}d +%Y-%m-%d); ...同じ確認...; done
IndexNowは200だったURLだけ
if [ "$OK" -gt 0 ]; then
node scripts/indexnow_submit.mjs --date "$TODAY_UTC" >> "$LOG" 2>&1 || say "WARN: IndexNow 送信に失敗した"
else
say "IndexNow: 本日分の200が0本のため送信しない"
fi
3. 改修の履歴(何が起きて足したか)
| 日付 | 足したもの | きっかけ |
|---|---|---|
| 8月28日 | 毎朝のビルドとデプロイ | 予約公開を入れたが、日付が変わっても再ビルドしないと記事が出ない(当時のコメントではISRで最大1時間、現在はdynamicParams = falseで再ビルドまで404) |
| 8月28〜29日 | 個別ページを完全静的に戻す(dynamicParams = false) |
ISRにすると実行時にcontent/news/*.mdを読む必要が生まれ、outputFileTracingIncludesに入れてもlambdaから読めずnotFound()=404になる。この2日、実際に本番の記事が落ちた(コードのコメントに残っている) |
| 9月4日 | 古いロックの自動解除 | 前回の異常終了でロックが残り、毎朝スキップし続けていたのに誰も気づかなかった |
| 9月4日 | Desktopへの失敗通知 | ログに書くだけでは「記事が出ていない」ことでしか気づけなかった |
| 9月4日 | npm testの実行 |
どこからも自動実行されておらず、落ちたテストが放置されていた |
| 9月4日 | 直近3日分の200確認 | GSCの全数調査で、再ビルド前にGoogleがクロールして404を記録した記事が11本見つかった。9:10より後に書いた当日日付の記事は翌朝まで出ないため |
| 9月4日 | IndexNow送信 | 鍵は8月14日に置いていたが送る仕組みが無く、1日90本公開しても検索エンジンに知らせていなかった(IndexNowだけでは足りなかった記録、Googleは参加していない) |
4. 停止するとき・手で直すとき
- 停止は3点すべてを確認する:①
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.ai-jitan-daily-deploy.plist; echo $?でexit 0、②plistを~/Library/LaunchAgents.disabled/へmvしてlsで確認、③launchctl list | grep ai-jitanが空。bootoutだけだと再起動時に再ロードされる - 手で直す:
cd ~/projects/ai-jitan-hub && npm run build && vercel --prod --yes(通知ファイルにも同じ1行を書いてある) - ログ:
~/logs/ai_jitan_daily_deploy.logに「開始/test OK/build OK/deploy OK/本番トップ: 200/本日分の確認: 200=N/直近3日分の確認/IndexNow」の順で1行ずつ残る
筆者が確かめた点
この記事は、筆者(当サイトの運営者)のMacで実際に動いているスクリプトと、9月17日の実行ログ(09:10:05開始・本日公開予定10本・09:13:30 deploy OK・09:13:45 IndexNow送信OK)を材料に書いた。抜粋したコードはスクリプトの該当行を短くしたもので、IndexNowの鍵やトークンは含まない。9月14〜16日は「本日公開予定=0本」で、IndexNowは送信していない。
書かれていないこと
- Vercel CLIの認証の設定手順(
vercel login済みのMacで動かしている前提) - Macがスリープしていた場合のlaunchdの挙動(当サイトのMacは常時起動)
- Vercel側のCron JobsやGitHub Actionsで同じことをする方法(当サイトはgitでなく作業ツリーを直接デプロイするため、Mac側で回している)
出典は当サイトのスクリプトとログ
~/bin/ai_jitan_daily_deploy.sh(2026年8月28日設置・9月4日改修)と~/logs/ai_jitan_daily_deploy.logsrc/lib/posts.tsのpublishCutoff/isPublished、src/app/news/[slug]/page.tsxのdynamicParams = falseとそのコメント- 本記事は仕組みを変えたら更新する
出典・参照資料
AIニュースの解説を動画でも
YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。
コメント
まだコメントはありません。最初のコメントを書いてみませんか?
AIについて聞きたいことはありますか?
質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。
質問箱を見る →新しい記事をメールで受け取る
AIの新しい発表を、出典付きで整理して届けます。