静的サイトの記事を、決めた日の朝に自動で出す仕組み
warp-studio.com は、書き終えた制作ノートと製品ページを、公開日の朝9時に自動で公開しています。公開してよいかの判定、出す前の検査、旧 URL の転送の自動更新、Windows のタスク スケジューラ、作る途中でつまずいた点の記録です。
warp-studio.com は、HTML を前もって作り、そのまま配る静的なサイトです。配信の側には今日の日付で記事を出し分ける仕組みがないので、決まった日に記事を出すには、その日にビルドして本番へ反映(デプロイ)し直すしかありません。2026年10月6日、書き終えた制作ノートと製品ページを、公開日の朝9時に自動で出す仕組みを作りました。先の日付の状態を手元で試して前もって見つけた失敗と、試し直さなかったために初回の当日まで気づかなかった失敗の記録です。
予約公開を作った理由
10月6日の夜の時点で、公開日を決めたものが12件ありました。制作ノートが10本、製品ページが2本です。Google AdSense(Google の広告配信サービス)の審査に落ちたあと、再申請に向けて AI が立てた計画で、記事はまとめて出さず、3〜4週間かけて公開することになっていました。AI は10月8日から23日までの6日に2件ずつ割り振り、その日の朝に PC が自動で出す形にしました。私の指示は「日程どおり進めて」です。
| 日付 | 出るもの |
|---|---|
| 10/8 | 製品ページ「引っ越し手続きチェック」、英会話アプリの改名のノート |
| 10/10 | 英会話アプリの AI 原価のノート、製品ページ「SubTrim」 |
| 10/13 | ノート2本 |
| 10/16 | ノート2本 |
| 10/20 | ノート2本 |
| 10/23 | ノート2本 |
公開してよいかを決める2つの値
記事と製品ページは Markdown で書き、先頭の front matter(題名や日付を書く欄)に status と publishedAt を持たせています。status の draft は AI の下書き、review は書いた本人とは別の AI の確認を通り、私の確認を待つもの、published は私が事実を確かめたものです。publishedAt は日本時間の公開日です。
本番に出すかどうかは、次の2行で決まります。
export const todayInJapan = () => process.env.SITE_TODAY ?? new Date(Date.now() + 9 * 3600 * 1000).toISOString().slice(0, 10);
export const isLive = (item, today = todayInJapan()) => item.data.status === 'published' && (!item.data.publishedAt || item.data.publishedAt <= today);
published で、しかも公開日が日本時間の今日以前なら出します。環境変数 SITE_TODAY があれば、そちらを今日として使います。条件を「今日以前」にしたので、公開日の朝に PC が止まっていても、次にビルドしてデプロイしたときに出ます。出さない記事は、サイトマップや RSS、一覧からも外れます。
本文では、根拠を確かめきれない箇所に【要確認】、私が自分の言葉を足す箇所に【運営者の一言】の目印を入れて書きます。published なのに目印が残っていると、ビルドそのものがエラーで止まります。
出す前の2つの検査
配信には Firebase Hosting を使っています。設定ファイル firebase.json の predeploy(デプロイの直前に走らせるコマンド)に、npm run build:deploy と npm run check:release を並べました。どちらかが失敗すると、デプロイは始まりません。
| 検査 | 走るところ | 主に見るもの |
|---|---|---|
| check-local-site | ふつうのビルドと build:deploy の中 | 内部リンクと画像、canonical(正規の URL の指定)、サイトマップとページの数の一致、RSS、転送の行き先 |
| check-release | predeploy の最後 | 目印、検索結果に出さない指定、下書きの帯、字数の下限 |
字数の下限は、公開中の制作ノートが本文2,000字、製品ページが2,500字で、下回るとその日の公開は止まります。
旧 URL の転送と、Firebase が設定を読む順番
10月5日から6日の作り直しで、昔の制作物ごとの紹介ページ(/apps/<名前>)を /apps の1ページにまとめ、古い URL は 301 転送(恒久的な移動の知らせ)で該当箇所へ送ることにしました。作り直しの時点で、転送は16件でした。
このうち2つの制作物は、10/8 の「引っ越し手続きチェック」と 10/10 の「SubTrim」として、製品ページで戻ってきます。Firebase Hosting は転送を静的なファイルより先に処理するので、転送が残ると新しいページが隠れます。そこで、ビルドがその日に公開中の製品ページを見て、firebase.json の転送を書き換えるようにしました。
ここに落とし穴がありました。Firebase のデプロイのコマンドは、predeploy を走らせる前に firebase.json を読み込みます。predeploy の中のビルドが転送を書き換えても、そのデプロイには書き換える前の設定が使われます。そこで、--deploy を付けたビルドは、firebase.json を書き換えたら「DEPLOY STOPPED」と出して止まるようにしました。同じコマンドをもう一度実行すれば、新しい設定で出ます。
予約公開のスクリプトは、デプロイの前にふつうのビルドを1回走らせ、そこで転送を書き換えておきます。それでも止まったときだけ、1回やり直します。
$deploy = Invoke-Step "firebase deploy --project $Project --only $Target --non-interactive"
if ($deploy.Code -ne 0 -and $deploy.Output -match 'DEPLOY STOPPED') {
$deploy = Invoke-Step "firebase deploy --project $Project --only $Target --non-interactive"
}
朝9時に起動して、出したあとも確かめる
起動には Windows のタスク スケジューラを使い、10月6日に、公開日ごとの 9:00 に scheduled-publish.ps1 を動かすトリガーを6つ登録しました。StartWhenAvailable(予定の時刻を過ぎていたら、動ける状態になりしだい実行する設定)も有効にしました。9時に PC が止まっていても、起動して私がログオンしたあとで実行される設定です。10月23日までの6回の公開日は、どれも9時に PC が動いていたので、止まっていた日の動きはまだ確かめられていません。
スクリプトは、ビルド、デプロイ、公開後の確認の順に進みます。公開後の確認(check-published-links.ps1)は、サイトマップのページと転送と RSS を本番から取り、200 や canonical、301 の転送先を確かめます。結果は実行ごとのログと、1行ずつ追記する summary.log に残します。
| summary.log の結果 | 意味 |
|---|---|
| OK | 出して、公開後の確認も通った。字数の下限を満たす公開中のノートと製品ページの数を添える |
| FAILED at build | ビルドか検査で止まった。本番は変わらない |
| FAILED at deploy | 公開前の検査かデプロイで止まった。Firebase が反映を終えたときにログに出す行(release complete)が無ければ、本番は変わらない |
| DEPLOYED but the published-site check FAILED | 出したが、公開後の確認で問題が見つかった |
先の日付を手元で再現して見つけたこと
先の日付のサイトを確かめるときは、SITE_TODAY に日付を入れてビルドします。10月6日には、公開日にあたる6つの日付(10月8日〜23日)でサイトを作り、検査を流しました。
このとき、10月23日の状態で check-release が落ちました。そのころ10月23日に出す予定にしていた制作ノートの1本が、以前の紹介ページの「動作確認中」という表示を引用していたためです。検査は、再申請の計画で消すと決めたこの表示を、すべてのページで探していました。そのままなら、10月23日の朝の公開は検査で止まっていたはずです。検査の範囲を、制作ノートの各ページ(/notes/ の下)を除く全ページに変え、直したあとは10月23日と30日の状態でも確かめました。
一方で、再現をやり直さなかったための見落としもありました。10月8日の未明(1時台)、この記事を確かめていた別の AI がその日の状態でビルドすると、check-local-site が「apps: missing archive entry hikkoshi-check」で止まりました。/apps に昔の制作物ごとの行き先(id)があるかを見る確認です。このままなら、9時の初回はビルドで失敗するところでした。原因は10月6日の夜のデザインの改善で、製品ページが公開された制作物を「過去の制作」の一覧から外したため、/apps#hikkoshi-check の行き先が消えていました。日付を進める再現は、この変更より前に一度やっただけでした。製品の枠(タイル)に同じ id を付け、10月8日から11月10日までの公開日ごとに SITE_TODAY を変えてビルドと2つの検査を流し直すと、すべて PASS でした。再現は、変更のたびにやり直さないと効きません。
ほかにも2つつまずきました。1つ目は下書きのプレビューの後始末です。下書きも含めてページを作るプレビュー(npm run preview:drafts)のあと、ふつうのビルドは、記事のページと RSS は消していましたが、一覧の notes.html は消さず、公開中の記事が無い当時は作り直されもしないので、プレビューの一覧が残りました。check-local-site がリンク切れとページ数の食い違いを見つけ、ビルドの最初に notes.html も消すようにしました。
2つ目はテストの前提です。試遊ボタンのテストは、サイトの今の設定のまま「公開を確かめていない試遊にはボタンを出さない」ことを見ていたため、試遊版の公開を確かめて設定を切り替えると、正しい動きなのに落ちました。今の設定を確かめるテストと、値をテストの中で渡すテストに分けています。公開日のテストも、日付を引数で渡します。predeploy ではテストも走るので、今日の日付や今の設定に頼るテストは、ある朝にデプロイを止めかねません。
初回の結果
タスクを登録した10月6日の夜、スクリプトを手で2回動かし、どちらも OK でした。公開後の確認は、どちらも13ページで PASS です。
予約した日付での最初の実行は、10月8日の9時です。引っ越し手続きチェックの製品ページと改名のノートが公開され、旧 URL の転送が初めて書き換わる日でもあります。10月7日に済ませた ads.txt と方針ページの変更も、この回で本番へ出る予定でした。前の節のビルドが止まる原因は、同じ日の1時台に直しています。
10月8日の9時にタスクが起動し、1分ほどで終わりました。ビルドと2つの検査は PASS で、firebase.json の書き換えは前夜のビルドで済んでいたため、デプロイは止まらずに1回で通りました。公開後の確認も15ページで PASS、summary.log の結果は OK です。引っ越し手続きチェックの製品ページと改名のノートが公開され、旧 URL の /apps/hikkoshi-check は転送ではなく製品ページそのものになりました。転送は、この日に15件に減りました。10月10日に SubTrim の製品ページが出て、もう1件減り、10月24日の時点で14件です。
これから
課題は3つあります。1つ目は日付です。10月6日に登録したトリガーは、10月23日までの6日分でした。そのあと公開日を決めた3本の分(10月27日・11月3日・11月10日)は、10月8日にタスクへ足しました。記事の公開日とタスクの日付は自動ではそろわないので、公開日を決めるたびに足す必要があります。10月24日に、最近の結果を書いたノートは書き上げてから数日のうちに出すと決めたので、足す回数はこれから増えます。2つ目は出す中身です。スクリプトはその朝の作業ツリー(手元のファイル)をそのまま出すので、作業中の変更もいっしょに出ます。3つ目は知らせです。失敗しても通知は来ず、summary.log を開くまで分かりません。
リリースの手順は、人の記憶より仕組みで止めるほうが安全だと考えています。AI のコーディング支援で、個人でもその仕組みを1日で用意できました。ただ、最初の朝の結果は、ログを開くまで分かりません。
記事の中身の誤りを公開前に機械で止める話は、AI が書いた記事の検査の制作ノートに書きました。この仕組みが止めるのも形の決まった誤りだけで、事実の確認は私が行います。