起
知っていることと、まにあうことは別だった
毎朝、日向坂46のその日の予定をまとめて投稿しています。続けるうちに、フォロワーさんからひとつの声をもらいました。「まとめは助かるけど、結局その時間を忘れてしまう。リマインドしてくれたら、もっと助かるのに」。たしかに、と思いました。予定を知っていることと、その時間にまにあうことは別のことです。
Webサービス 開発: 森下航 / 運用: 明治時代のせんとくん
日向坂46の予定を1枚に並べて、出演の前に知らせます。登録もログインも要りません。
実物を見る hinata-calendar.com
起
毎朝、日向坂46のその日の予定をまとめて投稿しています。続けるうちに、フォロワーさんからひとつの声をもらいました。「まとめは助かるけど、結局その時間を忘れてしまう。リマインドしてくれたら、もっと助かるのに」。たしかに、と思いました。予定を知っていることと、その時間にまにあうことは別のことです。
承
v1.8までは「Googleカレンダーに予定を書き込み、ユーザーはそれを購読する」方式で考えていました。これは廃止しています。通知のタイミングと見た目がGoogleカレンダー側の設定に握られてしまい、「推しの予定だけ、好きな時間に」という肝心のところが作れないためです。書き込み先アカウントの管理とAPIの割り当ても運用に残ります。v2.0では自前でカレンダーを描き、通知はWeb Pushで自分から出す形にしました。
転
公開の2日後、運営画面を見ると、通知が届かなかった件数が38件と出ていました。調べると、公開以降の失敗は100%がno_subscription——通知をONにしたが購読を持たない人ぶんで、Pushそのものの失敗は1件もありませんでした。設計上の正常値です。ただし同時に、未発火のキュー4,075件のうち2,233件(23人・55%)が購読の無い人ぶんだと分かりました。1分あたりの送信上限20件には2026-09-09に実際に当たっています。購読の無い人が送信の枠を食う構造はそのまま残っていて、直していません。
結
推しを選ぶと、その出演の前に通知が届きます。テレビ・ラジオ・配信は開始の15〜60分前から選べ、舞台やライブと終日の予定はその日の朝8時です。推しを選んでもほかの予定は消えません。カレンダーは全件を出したまま、通知だけを絞る作りにしてあります。深夜の番組は番組表と同じ感覚で前日の欄に並びます。
予定を集める仕事は持っていません。既存の HinatazakaSchedule が毎朝スナップショットを POST /api/ingest で投げ、こちらは受け取ってD1に入れます。画面は同じWorkerがStatic Assetsとして配り、/api/* だけをWorkerのコードへ回しています。通知は発火時刻を先に計算してキューに積み、毎分のCronが時間の来たものを拾ってWeb Pushで送ります。
Web Pushの本文暗号化には古い aesgcm(draft-04)と現行の aes128gcm(RFC 8291)があり、AppleのWeb Pushは後者しか受け付けません。ところがCloudflare Workers向けとして広く使われているライブラリは、2026-08-31に実装を展開して確かめた時点でどれも aesgcm でした。そこで @mmmike/web-push を版固定で採っています。ここが静かに戻るとiOSだけが黙って死ぬので、test/push.test.ts がヘッダと本体を実際に組み立てて Content-Encoding を見張っています。送信は DeliveryAdapter の裏に隔離してあり、最悪はRFC 8291を自前実装に差し替えられます。
供給側の category は閉じた列挙ではなく自由文字列で、実測291件のうち舞台81・雑誌19・イベント29の計129件(44%)が仕様上の「その他」に落ちました。区分を細かくする道もありましたが、通知バケットを2つに畳むほうを選んでいます。テレビ・ラジオ・配信は開始前、それ以外は当日8時です。開演19:30の舞台が朝8時に通知されるのは意図した挙動で、この種の予定は「直前」より「その日の朝に思い出す」ほうが合うという判断です。原文は category_raw に残してあるので、後から細かくしたくなっても情報は消えていません。
着手前に、同じことをする実装をワークスペース全体から探しています。Workers+D1の運用はHinatazakaAnalytics、SPAフォールバックはNeage、Web Push/VAPIDはRiverheadに前例があり、それぞれ踏襲・再利用しました。日向坂の予定収集はHinatazakaScheduleがあるので再スクレイピングはしていません。実装はClaude Codeと対話しながら進め、供給側との受け渡しは docs/ingest-contract.md に契約として先に文章で固めてから両側を直しています。人間が持っているのは、何を作らないかの決定と、実データを見てからの判断です。
構成: Cloudflare Workers(Static Assets+同一WorkerのAPI)/ D1 / Workers Cron / Web Push(VAPID・RFC 8291)/ Vite+React+TypeScript+Tailwind。テストは Vitest の29ファイル。
供給は毎朝7時すぎに1回入り、受け取った時刻はヘッダーに出ます。通知のCronは毎分動いていて、確認した時点の遅れは12秒でした。運営画面はローカル専用で、本番のデータは読むだけです。予定の中身は毎朝ひとつずつ人の目で確認し、漏れや誤りを直してから届けています。
数字は2026-09-22に本番の /api/health・/api/stats とリポジトリで測った値です。通知ON数など公開していない数字は載せていません。