AI時短ラボ
活用· 約14

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に公開・送信している。

Next.jsのサイトを毎朝自動でビルド・デプロイして「予約公開」を成立させる──Mac1台のlaunchd+Vercel CLIで、二重起動ロック・失敗をDesktopに通知・本番URLの200確認・直近3日の取りこぼし確認・IndexNow送信まで。当サイトが2026年8月28日から動かしている仕組み
執筆・編集:
目次

2026年9月18日・日本時間時点の情報です。 「記事を先の日付で仕込んでおいて、その日になったら自動で出したい」は、Next.jsの静的サイトで意外と面倒な要件で、当サイトは2026年8月27日に予約公開を入れてから、翌28日に「毎朝1回ビルドしてデプロイする」仕組みをMac1台のlaunchdで動かしている。この記事は、その仕組みを動かしているシェルスクリプトの構成を、当サイトの実際のコードと実行ログを材料に説明する。Vercelへのデプロイ前の一般的なチェックはチェックリストの記事、Vercel CLIが「gitでなく作業ツリーをそのまま上げる」点はこちらに。

3行まとめ

  1. なぜ毎朝ビルドするのか。 予約公開は「frontmatterのdateUTCの今日以下の記事だけを一覧・sitemap・個別ページに出す」だけの単純な実装(isPublished(date) = date <= today)。だが個別ページはビルド時にgenerateStaticParamsで公開済みの分だけ静的生成され(現在はdynamicParams = falseなので、ビルドに含まれなかった記事は再ビルドまで404のまま。8月28日にスクリプトを書いた時点のコメントでは「ISRのrevalidate 3600で、誰かが先に404を踏むとキャッシュが切れるまで最大1時間出ない」だった)、日付が変わっただけでは出ない。ビルドし直せばその日の分が静的生成されるので、毎朝1回ビルドしてデプロイする。Google側の「同じ日に大量に出す」ことへの懸念(scaled content abuse)を避けて小分けに出す運用も、この仕組みが支える。
  2. スクリプトの骨格(bash・約150行)。 launchdのStartCalendarIntervalで毎朝9:10に起動→mkdirでロック(前回が3時間より長く残っていれば異常終了とみなして外す。そうしないと「毎朝黙ってスキップし続ける」)→npm test(失敗しても公開は続行し警告だけ)→npm run build(失敗したらデプロイしない)→vercel --prod --yes本番トップと当日公開分のURLをcurlで叩いて200を数える直近3日分の記事も全部200か確認当日分で200だったURLだけIndexNowに送る
  3. 人が気づけるようにする。 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.shStartCalendarIntervalHour 9 / Minute 10WorkingDirectoryにリポジトリ、StandardOutPathStandardErrorPath~/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.log
  • src/lib/posts.tspublishCutoffisPublishedsrc/app/news/[slug]/page.tsxdynamicParams = falseとそのコメント
  • 本記事は仕組みを変えたら更新する
シェア: ポスト はてブ

出典・参照資料

AIニュースの解説を動画でも

YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。

コメント

まだコメントはありません。最初のコメントを書いてみませんか?

AIについて聞きたいことはありますか?

質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。

質問箱を見る →

新しい記事をメールで受け取る

AIの新しい発表を、出典付きで整理して届けます。

関連記事

Vercelデプロイのエラーを減らす個人開発チェックリスト──ローカルビルドから本番200確認までの記事画像
活用08.22読了7

Vercelデプロイのエラーを減らす個人開発チェックリスト──ローカルビルドから本番200確認まで

出典 ─ Vercel Docs: Troublesh
Vercel CLIはgitではなく作業ツリーを直接デプロイする──git statusが本番の中身を表していない状態の見分け方の記事画像
活用08.14読了29

Vercel CLIはgitではなく作業ツリーを直接デプロイする──git statusが本番の中身を表していない状態の見分け方

出典 ─ Exclude Files from Dep
IndexNowで418URL送ってもBing検索で40本中0本だった──Webmaster APIで確認した記録の記事画像
活用08.14読了27

IndexNowで418URL送ってもBing検索で40本中0本だった──Webmaster APIで確認した記録

出典 ─ IndexNow - Documentati
IndexNowにGoogleは参加していない──418URL送ってもGoogleには渡らない仕組みと、鍵が無効なときに返る403 UserForbiddedToAccessSiteの記事画像
活用08.14読了42

IndexNowにGoogleは参加していない──418URL送ってもGoogleには渡らない仕組みと、鍵が無効なときに返る403 UserForbiddedToAccessSite

出典 ─ IndexNow - Home(indexn
Claudeの使い方【2026年9月版】──無料でできること、Pro($20)とMax($100/$200)の違い、モデル(Fable 5.1・Opus 5・Sonnet 5・Haiku)の選び方、Claude Code・Cowork・Design・Chrome・Managed Agentsの入口を当サイトの記事30本でつなぐの記事画像
活用09.20読了14

Claudeの使い方【2026年9月版】──無料でできること、Pro($20)とMax($100/$200)の違い、モデル(Fable 5.1・Opus 5・Sonnet 5・Haiku)の選び方、Claude Code・Cowork・Design・Chrome・Managed Agentsの入口を当サイトの記事30本でつなぐ

出典 ─ Claude 料金ページ(プラン別の機能表・
非エンジニアがAI生成コードを本番に出す前に見る判断基準──検証ゲートで欠陥率が81%から0%になった実測をもとにの記事画像
活用09.05読了12

非エンジニアがAI生成コードを本番に出す前に見る判断基準──検証ゲートで欠陥率が81%から0%になった実測をもとに

出典 ─ Claude Code Docs(日本語)
非エンジニアがAIコーディングで事故った話──ビルド地雷の記録の記事画像
活用09.04読了13

非エンジニアがAIコーディングで事故った話──ビルド地雷の記録

出典 ─ Claude Code Docs(日本語)
非エンジニアがClaude Codeで自社サイト運用を回した実測記録──書く作業より確認する作業の方が多かったの記事画像
活用09.03読了13

非エンジニアがClaude Codeで自社サイト運用を回した実測記録──書く作業より確認する作業の方が多かった

出典 ─ Claude Code Docs(日本語)