「note 自動投稿」の仕組みを作って、noteをやめた話 ― 毎朝5つの定時タスクが記事をサイトへ運ぶまで

このサイトの裏側 公開

「note 自動投稿」で検索する人がまず知りたいのは、たぶん「どこまで自動化できるのか」「どこで人の手が要るのか」だと思う。結論から言うと、うちは全部自動化できた。ただし、いまはnoteに投稿していない。

このサイト(kyoteilog.com)は競艇の予想と分析記事を毎日1本出しているが、記事の生成から公開までを担う定時タスクは、もともと「noteへ自動投稿する仕組み」として作った。2026年8月末に組み上げ、9月頭から実際に毎晩noteへ予約投稿していた。ところが2026年9月22日、noteへの新規投稿を止めた。理由は単純で、note標準アカウントでは検索流入の計測ができず、Search ConsoleやGA4を見て「次に何を書くか」を決める仕組みと噛み合わなかったから(note pro自体が月¥80,000、そこにGA連携で月¥10,000が追加でかかり、合計で月¥90,000)。パイプラインの投稿先を1段だけ外し、生成した記事をそのまま自社サイトへ流すように変えた。仕組み自体はそのまま生きている。

この記事では、いま毎朝7時から8時台にかけて何が自動で起きているかを、実際に動いているスクリプトを読みながら順に追う。踏んだ罠も、ログとコード内のコメントに残っている実例だけを挙げる。


結論: 毎朝、人の手を触れずに記事が1本できて公開される

5つの定時タスクが連鎖して、次のことが起きる。

時刻 タスク名 ランナー やること
07:15 Kyotei_PredayPost run_preday.ps1 今日のレースを予想し、記事本文と台帳(JSON)を作る
07:40 Kyotei_SiteDaily run_site_daily.ps1 レビュー合格済みの分析記事をサイトへ出す → サイト全体を生成 → 公開 → 配信中の全URLが200を返すか確認
07:45 Kyotei_SeoBrief run_seo_brief.ps1 Search ConsoleとGA4を読み、今日どの記事を書くかの指示書を作る
08:00〜 Kyotei_ArticleWriter write_next_article.ps1 指示書と台帳を見て、次の分析記事を1本、サブエージェントに書かせる
(廃止・13:00〜) Kyotei_ArticlePost post_next_article.ps1 noteへ下書き投入→予約。2026-09-22にDisabled

一番下の「noteへ投稿する段」だけが、いまは動いていない。それ以外は毎朝ノーカットで動いている。

なぜこの順番なのか、なぜここで詰まったのか、を以下で1段ずつ見ていく。


07:15 Kyotei_PredayPost ― 今日のレースを予想し、noteへの投稿だけをやめた

run_preday.ps1 は本来「前日予想をまとめて回すランナー」で、当初は前夜22:30に翌日ぶんを作っていた。それを2026-09-01に早朝07:15へ移設している。理由はデータの鮮度だった。

前夜22:30に走らせると、前日のレース結果の答え合わせに使えるのは速報値(live_result)だけで、実測で144レース中134レースしか確定していなかった。一方、朝07:00の別タスク(Kyotei_DailyData)が公式の確定結果を取り込むので、その後に走らせれば答え合わせが確定値になる。今日ぶんの番組表は前夜20時台には公開済みなので、早朝に取りに行っても間に合う。「前夜に翌日を書く」から「早朝に今日を書く」への切り替えが、このタスクの最初の設計判断だった。

やっていることは4段階で、コード上の順番そのまま:

  1. python -m src.prediction.preday で今日の番組表から予想を作る。出力は reports/preday/<日付>.md(記事本文)と reports/preday/<日付>.json(台帳。翌朝Kyotei_SiteDailyが会場ページを作る元データになる)。
  2. (旧)noteへ下書きとして投入。
  3. (旧)今日20:00の予約投稿を設定。
  4. (旧)予約が本当に入ったかをAPIで確認。

2026-09-22のnote撤退後は、1で止まる。台帳(JSON)と記事本文(MD)までは今までどおり作るが、2〜4(noteへの投入・予約)はしない。台帳が無いと翌朝のサイト生成で会場ページが作れないので、ここは止めていない。コード上は $NoteRetired = $true という1つのフラグでこの分岐を作っていて、noteを再開するときはこのフラグを外すだけで元に戻る設計にしてある。

07:40 Kyotei_SiteDaily ― サイトを作り直し、公開し、配信を実測で確かめる

run_site_daily.ps1 は5段階。

  1. 記事をサイトへ出す: python -m src.web.kiji --promote を最初に呼ぶ。台帳(articles/queue.json)で「レビュー合格(reviewed)」になっている分析記事を「公開(published、published_on: "site")」へ昇格させる。08:00の執筆タスクより前に済ませるのがポイントで、そうしないと執筆タスクが「在庫が古い」と誤判定してしまう(後述)。
  2. 今日の予想台帳を待つ: reports/preday/<今日>.json が出るのを最大40分待つ。出なくても生成は止めず、「予想はまだ出ていません」の表示で続行し、Discordへ知らせる。
  3. 生成: python -m src.web.build で site/dist を別フォルダに作る。失敗しても前回配信中の版はそのまま残る。
  4. 公開: Cloudflare Pagesへ。APIトークンがあれば wrangler(Cloudflareの公式CLI)で出す。無ければ9222番ポートの共有Chromeを操作して画面から公開する(wranglerが失敗したときのフォールバックもこちら)。
  5. 配信の実測確認: 200が返っただけでは合格にしない。フッターに埋め込んだ「生成 YYYY-MM-DD HH:MM」の文字列が、公開直後に実際に読めるかを取得して確かめる。さらにsitemapに載っている全URLを毎回叩いて200を確認する。

5番目まで律儀にやる理由は後述の「罠」で説明する。

07:45 Kyotei_SeoBrief ― 次に何を書くかを機械が決める

run_seo_brief.ps1 は src.seo.brief を呼ぶだけの薄いランナーだが、やっていることはそれなりに重い。Search Console(過去28日のページ別・検索語別データ)とGA4(流入元)を取得し、さらにサイトマップの全URL(本記事執筆時点で51件・curl https://kyoteilog.com/sitemap.xmlで実測)の検索エンジンへの登録状況をURL検査APIで1件ずつ調べる。そのうえで「今日やること」を1つに決め、reports/seo/<今日>.md に書き出す。08:00の執筆タスクはこのファイルを読みに行く。

「今日やること」が決まらない日(Search Consoleの数字がまだ十分に溜まっていない日)は、後述する台帳の順番どおりに書く「規則3」に落ちる。これは失敗ではなく、指示書の設計上そうなる。

08:00〜 Kyotei_ArticleWriter ― サブエージェントに書かせ、3つの門番でデータの鮮度を守る

write_next_article.ps1 が一番作り込みが厚い。台帳の次の1本を選び、Claudeをヘッドレス実行(画面を開かず claude -p でプロンプトだけ渡して動かすこと)して記事を書かせ、レビューを通し、台帳を更新する。実行前に3つの門番を通す。

  1. 鮮度の門番: check_data_freshness.py で「前日ぶんのレース結果がDBに揃っているか」を見る。揃っていなければ書かずに終わり、後続のリトライ枠(10:00・12:00)に回す。揃っていない状態のまま書くと、記事の集計期間が古いまま公開されてしまう。
  2. 先行禁止の門番: 今日より先の日がすでに予約済みなら書かない。「今日書いて今日公開して、データは常に前日まで」という原則を守るための門番で、これが無いと在庫を先回りして積んでしまい、公開時点でデータが2〜3日古い記事が出る(実際に2026-09-05以前はこの状態だった。I04という記事は09-03に書いて09-05に公開されており、公開時点でデータが2日古かった)。
  3. 在庫の鮮度チェック: 前日の投稿が失敗して在庫が1本残っていた場合、その在庫の集計終端(data_cutoff)が「今日書くべき前日」と違えば、台帳のステータスをreviewedからideaに戻し、同じネタを最新データで書き直す。書いた原稿ファイル自体は上書きされるので、書いた材料そのものは捨てない。

3つとも通ったら、kyotei-writer(記事を書くサブエージェント)→note-article-reviewer(採点するサブエージェント)の順で1本を仕上げさせ、合格したら台帳のステータスをreviewedにする。ここで台帳に刻むdata_cutoff(集計に使った終端日)とwritten_at(執筆時刻)は、Claudeの自己申告ではなくランナー側が書く。理由は単純で、生成AIの「できました」という報告をそのまま信じて公開判定に使うと、実際には書けていない・古いデータのままの記事を通してしまうリスクがあるため。判定は必ず台帳のファイル状態(statusとmdファイルの実在)で行う。

本体のヘッドレス実行には約20〜24分かかる。

(廃止) 13:00〜 Kyotei_ArticlePost ― noteへの自動投稿はこうなっていた

いまは動いていないが、post_next_article.ps1 の中身も書いておく。noteへの自動投稿を検索している人にとってはここが本題のはずなので。

  1. 下書きへ投入(note_publish.js --new)。
  2. 構造点検(_structure_inplace.js): 見出し数・図の数が、書いた原稿と一致しているかをサイト側の見た目ではなくAPIレスポンスの構造で照合する。一致しなければ止める。
  3. 目次を挿入し、プレビューのスクリーンショットを保存。
  4. 予約投稿を設定(無料記事と有料記事で使うスクリプトが違う。有料記事は「ここから先は有料」の目印テキストが要る)。
  5. 予約が本当に入ったかをAPIで確認。

5段階すべてがNode.jsスクリプトで、Chromeを裏で動かして(CDP=Chrome DevTools Protocolでブラウザを直接操作する仕組み)note編集画面そのものの操作を自動化している。noteには公式の投稿APIが無いため、この形にせざるを得なかった。

2026-09-22にこのタスクと週報投稿タスクをDisabledにし、kiji.py側のpromote_reviewedが代わりにレビュー合格記事を直接サイトへ出すようにした。noteを再開する場合は、このタスクをEnableに戻し、kiji.pyのpromote処理を外すだけで元の経路に戻せる設計にしてある(作った側の判断として、撤退は「壊す」ではなく「1つのタスクを止める」で済むようにしておいた)。


踏んだ罠: 黙って失敗させないための実装

「動くコードを書く」より「失敗に気づける形にする」のほうに手間がかかった。実際にログとコードコメントに残っている実例を挙げる。

1. 月またぎの日付ピッカーで、初回の自動投稿がいきなり失敗した(2026-08-31) noteの予約投稿の日時ピッカーは当月のカレンダーグリッドしか正しく選べない。8月31日の実行で9月1日を予約しようとしたところ、旧実装は日番号のテキストだけで日付セルを探していたため、当月グリッドの隅に表示される「1日」(実は過去の8月1日)を掴んでしまい、押せない状態で止まった。修正は、ヘッダーが目標の月になるまで「次の月」ボタンを押してから、日付セルの完全な日付を持つ属性(例: "2026年9月1日火曜日")で選ぶように変えたこと。

2. stderrがエラー扱いになり、失敗の通知そのものが飛ばなかった(2026-08-31) PowerShell 5.1では、$ErrorActionPreference = 'Stop' の状態でNode.jsの標準エラー出力をパイプで直接受けると、その1行が「終端エラー」として扱われ、後続の「終了コードを見て失敗処理をする」コードに到達する前にスクリプトごと抜けてしまう。初回の自動実行はこれで失敗し、しかも「失敗した」という通知(Discord DM)自体が飛ばなかった。「予約が失敗すること」自体は許容していたが、「失敗に気づけないこと」は許容していなかったので、これは設計上の欠陥として扱った。修正は、Node.js呼び出しの前後だけ$ErrorActionPreferenceを一時的に緩め、成否を終了コードだけで判定するように直したこと。同じ穴は複数のランナーで見つかっており、いまは全部のNode.js/Python呼び出し箇所で同じ書き方を徹底している。

3. $argsという名前のパラメータが、PowerShellの組み込み変数と衝突していた(2026-09-03) 関数の引数名にうっかり$argsを使うと、明示的に宣言してもPowerShellの自動変数(束縛されなかった引数の配列)と衝突し、意図した値が渡らない。この結果、noteの下書き作成スクリプトが --new オプションを受け取れず、空のキーで編集画面を開こうとして固まった。エラーメッセージは「タイトル欄が表示されない」というタイムアウトにしか見えず、真因(引数名の衝突)から遠い場所に症状が出た。

4. PowerShell 5.1が、UTF-8の出力をShift_JISとして誤読していた(2026-09-02〜09-04) Node.js/Pythonの標準出力はUTF-8だが、PowerShell 5.1のコンソールは既定でコードページ932(Shift_JIS系)として読む。タスクスケジューラ経由(対話コンソール無し)の実行だとこれが顕著で、ログの日本語が文字化けするだけでなく、JSON文字列をそのままパースする箇所で壊れた文字列が渡って落ちることもあった。対策は[Console]::OutputEncodingをUTF-8に固定し、Python側にもPYTHONIOENCODING=utf-8を明示すること。

5. 公開ツールの「同梱するファイルの許可リスト」から新規ページが漏れ、サイトマップには載るのに本番は404だった(2026-09-20) サイト生成そのものは正しく新規ページを作っていたが、公開スクリプトが「どのフォルダを配信物に含めるか」を固定のリストで持っており、新しく作った3ページがそのリストに入っていなかった。結果、sitemap.xmlには載っているのに、実際に開くと404という食い違いが起きた。しかも公開処理自体は「成功」として終わっていた。確認先を固定URLで持っていたため、既存ページだけ見て「完了」と判定していたのが根本原因。修正は2つ: 公開ツール側は同梱物をフォルダ丸ごとにして「ZIPの項目数とディスクの実ファイル数が一致しなければ例外にする」形に変え、run_site_daily.ps1側は「配信中のsitemapから毎回全URLを取り出し、全部叩いて200を確認する」処理を追加した。「増えたページこそ確認の対象」という教訓をコードに落とし込んだ形。

6. 記事の公開状態をpublishedに戻す処理が、どこにも無かった(2026-09-20) 台帳のステータスは「予約済み(scheduled)」までしか自動で進まず、noteで実際に公開された後に「公開済み(published)」へ書き換える処理が存在しなかった。結果、実際には14本公開済みなのに、台帳だけ見ると「未公開が14本ある」ように見える状態が続いていた。noteの公開一覧とAPIで突き合わせて台帳を修正するスクリプトを作り、予約投稿の直後に必ず走らせる形にした(このステップが失敗しても、投稿自体の成功は取り消さない設計にした)。

7. バックグラウンド処理の待ちタイムアウトで、書き上がっていた記事ごと失敗扱いにしていた(2026-09-09) ヘッドレス実行のclaude -pは既定で600秒バックグラウンド処理を待ち、それを超えると打ち切りメッセージを標準エラーに出す。上の「2」と同じ理由でこの1行がエラー扱いされ、実際には記事が書き上がっていたケースでも「失敗」と判定して台帳をideaに戻していた(=せっかく書けていた成果を捨てて、次の実行枠がゼロから書き直していた)。対策は待ち上限を30分に延ばし、判定を終了コードだけに絞ったこと。

8. 裏で動くclaude -pのログインが切れ、定時ジョブが4日間まとめて止まった(2026-09-18〜22) ヘッドレス実行に使っているログインセッションの有効期限が切れ、執筆タスクが4日連続で動かなくなった。対策として、有効期限1年の長期トークン(claude setup-tokenで発行)を保存する方式に切り替えている。


定時タスクの登録は専用スクリプトを通す

これらのタスクをWindowsタスクスケジューラへ登録するとき、schtasksや管理画面を直接使わず、tools/scheduled-task-kit/register-task.ps1という専用スクリプトを必ず通す運用にしている。理由は、過去に手作業の登録で3件の事故が起きたため。

  1. あるタスクだけバッテリー動作時に起動しない設定・起動可用時に追いつく設定が両方オフのまま登録され、電源に関わる2つの起動拒否条件を同時に踏んでいた。
  2. 定時タスクの死活監視(このプロジェクトでは「番人」と呼んでいる仕組み)への登録が人の記憶頼りになっており、11個のタスクが監視対象から漏れたまま、そのうち4個が実際に止まっていた。
  3. 複数のタスクが同じChromeプロファイルへ同時にCDP接続する形で「作れてしまう」状態があった。

register-task.ps1は登録前にランナー本体を検査する。日本語を含むのにBOM無しUTF-8で保存されていないか(PowerShell 5.1がShift_JISと誤読して壊れる)、共有Chromeを使う宣言をしたタスクがロック機構を実際に呼んでいるか、監視の閾値(何時間動きがなければ異常とみなすか)を指定しているか、を機械的にチェックし、違反があれば登録自体を拒否する。「作法を徹底する」ではなく「手続きで強制する」方針に倒した結果である。


実績: note撤退後、何本がサイトへ直接自動公開されたか

台帳(articles/queue.json)を数え直した(2026-09-25時点、集計対象は台帳の全56エントリ)。

  • note経由で公開された記事: 14本(2026-09-03〜09-17)
  • noteを止めたあと、サイトへ直接自動公開された記事: 4本(2026-09-23〜09-25)

この4本のうち最初の2本(「何歳がピークで、いつ落ちるか」「『その日よく出る出目』を買い続けたら」)は、どちらも2026-09-22の夜(20:52・21:00)に書き上がっていた在庫で、翌23日朝のKyotei_SiteDailyがまとめて公開へ昇格させたため、公開日が同じ日に重なっている。これはnote撤退の切り替え作業中にできた一時的な在庫で、09-23以降は1日1本のリズムに戻っている(09-24公開分・09-25公開分は、それぞれ前日の朝までに1本ずつ書かれている)。

数字はすべて台帳のstatus・date・published_on・written_at列をRead/Grepで直接数え直したもので、計算の詳細はarticles/_work/i50_calc.mdに残している。


まとめ

「note 自動投稿」の仕組みは、作ること自体はできた。実際に3週間近く、下書き投入から予約確認までノーコードで回っていた。ただし公式の投稿APIが無いプラットフォームを自動化すると、画面のレイアウト変化(日付ピッカーの月境界など)に依存する脆さが残る。うちが最終的に離れた理由はそこではなく、「計測ができない」という、自動化の技術とは別の制約だった。

いま動いているのは、投稿先をnoteから自社サイトに差し替えた同じパイプラインである。予想を作る・記事を書く・サイトを作って公開する・配信を実測で確認する、という骨格は変わっていない。


免責

  • 本記事に記載した数値はすべて過去データに対する実測であり、将来の的中や利益を保証するものではありません。
  • 本記事および言及したツールは分析の補助を目的としており、舟券の購入を勧誘するものではありません。
  • 舟券は20歳未満の方は購入できません。
  • 舟券の購入は自己責任で、生活に必要な資金とは別の、余裕資金の範囲で行ってください。のめり込みには十分ご注意ください。

同じ分類の記事(このサイトの裏側)

分析記事の一覧へ