Blog 自動化

AI自動投稿パイプライン構築記 - 寝ている間にブログが更新される仕組み

毎月1日・15日の深夜3時、AIが記事を書いてこのブログに公開されるまでを人手ゼロで回す仕組みの構築記。ネタ切れ対策のキュー、Claude Codeのヘッドレス実行、cronの時差の罠、品質をプロンプトで縛る話まで。

8 min read

このブログは月2本、勝手に記事が増える

毎月1日と15日の深夜3時、私が寝ている間にこのブログに新しい記事が公開されます。ネタ選びも、執筆も、コミットもデプロイも人手ゼロ。記事一覧で「AI生成」バッジが付いているものが、全部この仕組みの仕事です。

作った動機は単純で、新卒向けの基礎記事を定期的に出したかったからです。ただ、本業のかたわらで月2本をずっと書き続けるのは、正直どこかで止まる未来が見えていました。続かないと分かっている作業は、最初から仕組みに落とす。この記事は、その仕組みの構築記です。

まず課題を分解する

「AIに記事を書かせて公開する」は、そのままでは大きすぎるので、いつもどおり分解から入ります(この「分解してから道具を当てる」進め方は別の記事に書きました)。

ピースやること使った道具
ネタ管理何を書くかを決めておく・使ったら消すJSONのキュー
本文生成テーマから記事本文を作るClaude Code(ヘッドレス)
記事ファイル化frontmatter付きのMarkdownにするTypeScriptスクリプト
公開決まった日時にコミット→デプロイGitHub Actions

割ってみると、どのピースにも既製の道具がはまります。ゼロから作ったものはほぼなくて、書いたのはつなぎ目のスクリプトとワークフロー定義だけです。

ネタはJSONのキューで管理する

一番地味で一番効いているのがここです。自動化しても、ネタが尽きたら止まります。なので書きたいテーマは topics.json に貯めておき、実行のたびに先頭の未使用テーマを1つ消費する方式にしました。

{
  "topics": [
    {
      "title": "正規表現入門 - 文字列処理の強い味方",
      "category": "技術解説",
      "tags": ["正規表現", "テキスト処理", "Tips"],
      "audience": "IT未経験の新卒エンジニア",
      "used": false
    }
  ]
}

used フラグを立てるだけの単純なキューですが、「ネタ出し」と「執筆」を切り離せるのがポイントです。ネタは思いついたときにまとめて追加しておけばよく、締切の夜に「今日は何を書こう」と悩む工程そのものが消えます。

生成はClaude Codeをヘッドレスで叩く

本文の生成には、コーディングエージェントのClaude Codeをヘッドレスモードで使っています。claude -p にプロンプトを渡すと、対話UIなしで応答だけが返ってくるモードで、要するにCLIを1回叩くだけでAIの出力が手に入ります。スクリプトからはこう呼んでいます。

// Spawn Claude Code in headless mode; prompt goes through stdin
const out = execFileSync(
  "claude",
  ["-p", "--model", "claude-opus-4-8", "--output-format", "text"],
  { input: prompt, encoding: "utf-8", timeout: 20 * 60 * 1000 },
);

CI上での認証は、claude setup-token で発行したトークンをGitHub Secretsに入れ、環境変数 CLAUDE_CODE_OAUTH_TOKEN で渡すだけです。これでGitHub Actionsのランナー上でもClaudeのサブスクリプション認証で動くので、記事生成が定額の範囲に収まります。APIを直接呼ぶ構成も作れますが(従量課金)、この仕組みではフォールバックに回しています。深夜の無人実行で一番困るのは「生成が失敗して投稿が落ちること」なので、CLI→API→別プロバイダの順に倒れる保険を入れました。

品質はプロンプトに書いた分しか出ない

このパイプラインで一番学びがあったのはここです。「新卒向けに分かりやすく書いて」程度の指示で出てくるのは、挨拶で始まり、概念の説明を飛ばしてコマンドを並べる、どこかで読んだような記事です。

AIの出力品質は、プロンプトに明文化した基準の分しか上がりません。いま生成プロンプトには、自分がレビューで直したくなる点をそのままルール化して入れています。

  • 概念→理由→操作の順で書く。それが何で、なぜ存在するかを説明する前にコマンドを見せない
  • 用語は初出で必ず定義する。挨拶・自己紹介で始めない
  • 初心者が実際につまずくポイントを、つまずく場所に置く
  • 破壊的な操作には、プロが使う安全手順(実行前の確認など)もセットで教える
  • 文体はです・ます調。絵文字・決まり文句の激励は禁止

もう1つ細かい工夫として、記事の要約文(frontmatterのdescription)は本文の切り出しではなく、出力の1行目に DESCRIPTION: として書かせています。本文の先頭120字を機械的に切り取る方式だと、文の途中でぶつ切りになった要約が検索結果に並ぶことになるからです。

cronの罠 - 「1日の深夜3時」は素直に書けない

公開日時は毎月1日・15日の3:00(日本時間)です。GitHub Actionsのスケジュールはcron式で書きますが、ここに時差の罠があります。cronはUTCで解釈されるので、日本時間の1日3:00は「前日の18:00 UTC」。そして「1日の前日」は月によって28日だったり31日だったりします。cron式では「月末日」を表現できません。

そこで、候補日すべてで起動して、実行時に日本時間で判定する方式にしました。

on:
  schedule:
    - cron: "0 18 14,28,29,30,31 * *" # 03:00 JST on the 15th / 1st
# Skip unless it is the 1st or 15th in JST
JST_DAY=$(TZ=Asia/Tokyo date +%-d)
if [ "$JST_DAY" != "1" ] && [ "$JST_DAY" != "15" ]; then
  echo "skip"; exit 0
fi

月に数回、起動して数秒でスキップする空実行が発生しますが、実害はありません。ちなみに深夜3時にしているのは、サブスクリプションの利用枠を自分が使っていない時間帯に消化させるためです。人間が寝ている間に働かせる、を文字通りやっています。

公開の罠 - botのpushはデプロイを起こさない

生成した記事はワークフロー内でコミット・プッシュします。ここにも罠が1つ。このブログはpushをトリガーに別のワークフローがビルド・デプロイする構成なのですが、GitHub Actionsが GITHUB_TOKEN で行ったpushは、他のワークフローのpushトリガーを発火させません(ワークフローの無限連鎖を防ぐGitHub側の仕様です)。

つまり、何も知らずに組むと記事だけコミットされて、サイトはいつまでも更新されません。対策として、コミット後にデプロイ用ワークフローを明示的に起動しています。

git push
gh workflow run deploy.yml --ref main # dispatch is exempt from the restriction

workflow_dispatch 経由の起動はこの制限の対象外なので、これで生成→公開までつながります。

本番前に試すdry_run

無人運用の仕組みには、本番に触らないテスト経路が要ります。ワークフローの手動実行に dry_run オプションを付けていて、オンにすると記事の生成までを行い、結果をログに全文表示して、コミットせずに終わります。

プロンプトを調整したいときはこれを回して、出力を読んで、基準を書き足して、また回す。生成AIを組み込んだ自動化は「作って終わり」にはならず、この品質のフィードバックループを持てるかどうかが実用性を分けると感じています。

まとめ

  • 「AIに記事を書かせて公開する」を分解すると、ネタ管理・生成・ファイル化・公開の4ピース。それぞれに既製の道具がはまり、自作したのはつなぎ目だけ
  • 生成品質はプロンプトに明文化した基準の分しか出ない。レビューで直したくなった点をルールとしてプロンプトに還元する
  • ハマりどころは「時差」(cronはUTC・月末日は書けない)と「権限」(botのpushはトリガーを発火しない)
  • 無人運用には dry_run のような本番に触らないテスト経路を最初から用意する

いまのところ、この仕組みは月2本のペースを黙々と守っています。次はネタ出しそのものをAIに任せてキューの補充まで自動化するか、生成された記事を別のAIに校閲させる自動レビューあたりを考えています。形になったら、また構築記を書きます。

※ 本記事の内容は執筆時点の情報であり、正確性を保証するものではありません。ご利用の際は免責事項をご確認ください。

Share

Related / 関連記事

関連記事

Comments / コメント

コメント