「プログラミングはできないけど、記事の投稿くらいAIに任せられないかな」
そんな軽い気持ちで始めたのが、今回の自動投稿づくりでした。私はコードをゼロから書けるタイプの人間ではありません。ターミナルの黒い画面を見るだけで、ちょっと身構えてしまう側の人間です。
そんな私が、Claude Codeに伴走してもらいながら、WordPressへ記事を自動投稿する仕組みを手探りで組み上げてみました。基本的な発想の下敷きにしたのは、Gemini CLIでリサーチをしつつ複数のAIツールを組み合わせてWordPressへ投稿する、という記事「https://www.m-totsu.com/1508/」です。
この記事の考え方をベースに、自分の環境と好みに合わせて作り替えていきました。
先に結論を言ってしまうと、Claude Codeにお願いしたらけっこう簡単にできました!
ただ、そこにたどり着くまでの道中は難しい単語が出てきて戸惑う場面も。
この記事は、その「つまずき全記録」です。同じところでこけそうな人が、私よりずっと短い時間で抜けられるように、恥ずかしい失敗も包み隠さず書いていきます。
ちなみにこの記事もClaude Codeに書いてもらって、そこから自分が修正しています。
そもそも何を作ったのか ― 完成した自動投稿パイプラインの全体像

細かい話に入る前に、まずは「結局どんなものができたのか」を見てもらった方がいいですね。
できあがったのは、Claude Codeで「/generate-post <テーマ>」 と打つだけで、リサーチから執筆、SEOチェック、WordPressへの下書き投稿までを一気通貫でこなしてくれるパイプラインです。テーマ(キーワード)を1つ渡せば、あとはツールたちが役割分担して働いてくれる、という形です。
全体の構造は、参考記事の5層アーキテクチャの考え方を下敷きにしています。上から下へ、おおよそ次のようなブロックが積み重なっているイメージです。
| 層 | 役割 |
|---|---|
| スキル層 | /generate-post という入口。全体の司令塔 |
| エージェント層 | アウトライン・執筆・SEOレビューを担うサブエージェント群 |
| Python連携層 | WordPressやAPIとやり取りする橋渡しスクリプト |
| CLI呼び出し層 | Gemini CLIなど外部ツールを叩く部分 |
| WordPress層 | REST API経由で下書きが積まれる最終出力先 |
そしてこの仕組みは、大きく5つの工程で動いています。どの工程をどのツールが担当し、Claudeのトークンを消費するのかどうかを表にまとめてみました。トークン消費を意識したのは、後述する「なるべく賢く節約したい」という思いからです。
| 工程 | 担当ツール | 使うモデル | Claudeトークン消費 |
|---|---|---|---|
| リサーチ | Gemini CLI | Gemini | 消費ゼロ |
| アウトライン設計 | サブエージェント | Opus | あり |
| 執筆 | サブエージェント | Opus | あり |
| SEOレビュー | サブエージェント | Opus | あり |
| 投稿 | wp_client.py | LLM不使用 | なし |
執筆工程では、WordPressのGutenbergエディタにそのまま貼り付けても崩れないブロック形式のHTMLを吐き出すようにしました。最後の投稿工程はあえてLLMを使わず、Pythonスクリプトが淡々とREST APIを叩くだけ。ここに知能を持たせないことが、実は安全運用のコツだったりします。
「下書き固定・自動公開しない」という安全方針
この仕組みには、絶対に譲れないルールを1つ決めました。それは「自動公開は絶対にしない」ということです。AIが書いた記事は必ず下書きとして積まれ、公開ボタンを押すのは最後まで人間の私自身です。
非エンジニアだからこそ、暴走が怖いんですよね。だからこそ「最終確認は必ず人間が行う」という一線だけは、最初から最後までブレさせませんでした。
なぜこの構成に落ち着いたのか ― 方針転換の記録
今でこそスッキリした構成に見えますが、ここに至るまでには何度か方針を変えています。最初から一直線に決めたわけではなく、むしろ迷いながら少しずつ形を整えていった、というのが正直なところです。
当初は参考記事にならって、Gemini CLI・Codex CLI・Claude Codeの3つのツールを併用する構成を検討していました。せっかくなら全部盛りで、と欲張ったわけです。でもすぐに「これ、トークン利用量がどんどん膨らむのでは」という不安がよぎりました。
そこでいったん、思い切ってClaude単体まで簡略化してみました。ところが今度は、リサーチまでClaudeにやらせるとトークンがもったいない。あちらを立てればこちらが立たず、という感じで、しばらく行ったり来たりしていました。
最終的にたどり着いたのが、「リサーチだけはGemini CLIに外出しして、残り3工程はOpusで統一する」という折衷案です。Claudeのトークンを一番食いそうな下調べをGeminiに任せることで、コストと品質のバランスがようやく腑に落ちました。
さらに後日、GetKeyword APIを使ったキーワード選定機能も追加しました。ロングテール寄りで検索ボリューム1000未満の狙い目を拾いつつ、記事同士が食い合わないようにカニバリ回避のクラスタリングをかける、という欲張りな機能です。
環境構築でこけまくった話
さて、ここからが本題です。この記事の中核である「つまずき」に入っていきます。どれも「何が起きたか」「原因は何だったか」「どう乗り越えたか」の3点セットで語っていくので、心当たりのある症状があればそこだけ拾い読みしてもらっても大丈夫です。
つまずき1:そもそもNode.jsが入っていなかった
記念すべき最初のつまずきは、拍子抜けするほど手前の地点でした。Gemini CLIを入れようとしたら、そもそも私のMacにはNode.jsもnpmもHomebrewも、何1つ入っていなかったのです。
今思えば当たり前なのですが、非エンジニアのパソコンには開発ツールなんて入っていないんですよね。「CLIをインストールする」以前に「CLIをインストールするための道具」から用意しなければいけないと知って、静かにため息をつきました。
結局、まずはNode.jsを入れるところからのスタートです。ゴールへの第一歩どころか、スタートラインの手前に立っていたことに気づかされた、そんな一話でした。ここで諦めなくてよかったです。
つまずき2:npmのグローバルインストールで権限エラー(EACCES)
Node.jsを入れて意気揚々と npm install -g @google/gemini-cli を実行したら、赤い文字で EACCES: permission denied と突き返されました。せっかく前に進めたと思った矢先の門前払いで、正直ここでつまずくとは思っていませんでした。
原因は、グローバルインストール先である /usr/local/lib/node_modules に書き込み権限がなかったことでした。エラーメッセージにも書いてあったのですが、最初は英語の羅列にしか見えず、意味を汲み取るのに少し時間がかかりました。
ここで安易に sudo を付けて無理やり通す手もあったのですが、権限まわりを力技でいじると後々ややこしくなると聞いたので避けました。代わりに、インストール先を自分のホームディレクトリに変更する方法を選びました。
具体的にやったことを表にしておきます。この2つを打っただけで、あっさり解決しました。
| 実行したコマンド | 目的 |
|---|---|
| npm config set prefix ~/.npm-global | インストール先をホーム配下に変更し権限問題を回避 |
| export PATH=~/.npm-global/bin:$PATH | 変更先のコマンドを認識させるためPATHに追加 |
つまずき3:WordPressのユーザー名が日本語でエラー地獄
WordPressに接続するため、.envファイルの WP_USER に自分の名前「野本一貴」を入れて実行したところ、いきなり UnicodeEncodeError でクラッシュしました。
調べてみると、HTTP Basic認証はlatin-1というエンコード方式が前提で、そもそも日本語の文字を扱えない仕様なのだそうです。名前を入れただけで怒られるとは思わず、面食らいました。
そこでClaude Codeと相談しながら、UTF-8をbase64エンコードして認証する処理に書き直しました。「これで日本語も通るはず」と自信満々で再実行したのですが、今度は無情の 401。認証そのものが弾かれてしまいます。
エンコードは直したのに、なぜ通らないのか。原因を1つずつ潰していってようやく気づいたのは、そもそも「野本一貴」は画面に出る表示名であって、ログインに使うユーザー名ではなかった、という事実でした。私の本当のログインユーザー名は、半角英数の「Kazuki Nomoto」だったのです。
正しいログインユーザー名に直したら、拍子抜けするほどすんなり認証が通りました。表示名とログイン名が別物だという、言われてみれば当たり前の落とし穴。非エンジニアが一番ハマりやすい罠だと思うので、声を大にしてお伝えしておきます。
つまずき4:.envファイルがシェルでパースできない
次にぶつかったのは、.envファイルの読み込みトラブルです。設定を反映させようと source .env を実行したら、「コマンドが見つかりません」というエラーが連発しました。設定を書いただけなのに、なぜコマンド扱いされるのか最初は謎でした。
原因は、値の中にスペースが含まれていたことでした。Bashで直接読み込むと、スペースが区切り文字として扱われ、後ろの単語が別のコマンドだと勘違いされてしまうんですね。ちなみにPythonのdotenvライブラリ経由なら問題なく読めるので、あくまでシェルで直読みしたときだけの現象でした。
解決はあっけないもので、スペースを含む値をダブルクォートで囲むだけ。たったこれだけのことなのですが、原因にたどり着くまでが地味に長かった一件です。
つまずき5:Gemini CLIが勝手にOAuthログインを使おうとした
認証まわりの沼はまだ続きます。APIキーをちゃんと設定したはずなのに、Gemini CLIを動かすと IneligibleTierError というエラーが出て止まってしまうのです。キーは合っているはずなのに、と何度も設定を見直しました。
犯人は、過去に一度試したOAuthログインの残骸でした。~/.gemini/settings.json という設定ファイルに selectedType: "oauth-personal" という記述が残っていて、APIキーよりもそちらが優先されていたのです。新しく設定したつもりが、古い設定に足を引っ張られていたわけですね。
解決策は、その設定ファイルを直接開いて "selectedType": "gemini-api-key" に書き換えること。こうしてAPIキー認証を強制したら、ようやくGeminiが素直に動いてくれました。過去の自分の実験が未来の自分を妨害してくる、という何とも言えない体験でした。
つまずき6:Gemini APIのクォータ超過(無料枠だと動かない)
認証を突破して「さあリサーチだ」と動かし始めたら、今度は 429 RESOURCE_EXHAUSTED や Your prepayment credits are depleted というメッセージが出るようになりました。要するに、使える枠を超えてしまった、という通知です。
原因は単純で、APIキーに紐づくプロジェクトの課金設定が済んでおらず、無料で使えるクレジットが尽きていたことでした。リサーチは一度に結構な量のやり取りをするので、無料枠だとあっという間に上限に届いてしまうんですね。
解決は、Google AI Studioの管理画面(https://aistudio.google.com/apikey)から課金設定を有効にすること。最低課金額の2000円をチャージ。ここで痛感したのは、「無料枠のままだと安定運用は難しい」という現実です。趣味の範囲でも、継続的に回すなら多少のコストは覚悟が必要だと学びました。
データとAPIまわりで悩んだ話
環境構築の山をなんとか越えたと思ったら、今度はデータの扱いやAPIの細かい挙動で新たな壁が待っていました。ここからは、キーワード選定機能を追加したあたりから増えていったつまずきたちです。
つまずき7:PythonだけがなぜかSSLエラー(GetKeyword API)
これはかなり悩まされた一件です。WordPressへの接続では問題なく動いていたPythonの requests が、GetKeyword APIを叩くときだけ毎回 SSLError: TLSV1_ALERT_PROTOCOL_VERSION で失敗するのです。同じライブラリなのに、相手が変わると通らない。理由がまるで見当たらず、しばらく途方に暮れました。
切り分けのために、同じアドレスへ curl で接続してみたところ、これはあっさり成功しました。Pythonでは失敗し、curlでは成功する。この食い違いが、原因特定の大きなヒントになりました。思考の途中経過を、表にしてみます。
| 試した方法 | 結果 | そこからわかること |
|---|---|---|
| curlで接続 | 成功 | サーバーや回線側の問題ではない |
| Python requestsで接続 | 失敗(SSLError) | Python側のTLS環境が怪しい |
真相は、Macに最初から入っていたPython3.9が、古いLibreSSL 2.8.3というライブラリにリンクされていて、相手サーバーが求める新しいTLSの要求を満たせなかった、というものでした。curlは別のより新しいTLS環境を使っていたので通っていたわけです。
本来ならPythonのTLS環境を入れ替えるのが筋なのでしょうが、私はもっと現実的な近道を選びました。requestsで頑張るのをやめて、成功していたcurlをサブプロセスとして呼び出す方式に切り替えたのです。少し泥臭いですが、確実に動くものを優先しました。
つまずき8:クラスタリングが暴走して1つの巨大グループに
キーワードを意味の近いもの同士でグループ分けする、クラスタリング機能を作ったときの話です。最初は「共通する単語が1つでもあれば同じグループにする」というシンプルなロジックで組みました。ところが結果を見て目を疑いました。
なんと、無関係なはずの878件がまとめて1つの巨大クラスタに飲み込まれていたのです。犯人は「windows」や「比較」といった、いろんな場面に登場する汎用的な単語でした。これらが橋渡し役になって、本来つながらないキーワード同士を数珠つなぎにしてしまったんですね。
解決策として、判定のルールを大きく変えました。「共通語が1つでもあれば」ではなく、「除外すべき汎用語の集合が完全に一致する場合だけ同じクラスタにする」という厳しめの条件にしたのです。これでほぼ1候補=1クラスタという、ちょうどいい粒度に落ち着きました。ゆるすぎる条件は、時に何もかもを一緒くたにしてしまうという学びでした。
つまずき9:Googleサジェストのノイズ(韓国俳優が出てくる)
キーワードの候補を広げるためにGoogleサジェストを利用したのですが、ここでちょっと笑ってしまう現象に出会いました。「SEO」で候補を取ると、なぜか俳優のSeo Kang Joonさんや、都市名のSeoulが混ざってくるのです。SEO対策を考えていたはずが、韓国俳優の名前が並ぶ画面を眺めることになりました。
原因は、キーワードが短すぎるとサジェストが暴発しやすいことでした。「SEO」という3文字が、たまたま人名や地名の一部と一致してしまうんですね。機械からすれば当然の挙動なのですが、人間から見るとなかなかシュールでした。
対策は、フィルタを強化して、トークン単位で完全に一致するものだけを採用するようにしたことです。これで無関係な人名や地名がすっと消え、狙ったジャンルの候補だけが残るようになりました。
つまずき10:WordPressカテゴリー名の表記ゆれ(大文字小文字1字違い)
これは本当に些細な、でも見つけるまでは厄介な一件でした。AIが生成したカテゴリー名「AI自動化・Claude code」が、WordPress側に実在する「AI自動化・Claude Code」と一致せず弾かれてしまったのです。よく見ると、cの大文字と小文字がたった1文字違うだけ。
原因はシンプルで、カテゴリー名の照合が完全一致を要求していたためです。人間の目にはほぼ同じに見える1文字の差が、機械にとっては別物として扱われた、というわけですね。
ここで効いたのが、安全設計の1つとして入れていた「存在しないカテゴリーは自動作成しない」という方針でした。もし自動作成する設計だったら、微妙に違う名前のカテゴリーが勝手に増殖していたはずです。今回は増えることなくエラーで止まってくれたので、私が1文字直すだけで解決しました。慎重な設計が、こういう地味な場面で救ってくれるんだなと実感しました。
Claude Codeそのものでハマった話
環境やデータだけでなく、Claude Codeという道具そのものの仕様でつまずいたこともあります。最後の1話は、そんなツールの挙動にまつわるエピソードです。
つまずき11:スラッシュコマンドがすぐには使えなかった
ようやくスキルの定義ファイル .claude/skills/generate-post/SKILL.md を作り終え、意気込んで /generate-post と打ち込みました。ところが返ってきたのは「Unknown command」。作ったばかりのコマンドが認識されず、一瞬「え、何か書き間違えた?」と焦りました。
原因は、スキルファイルがセッション開始時に読み込まれる仕様だったことです。つまり、作成した直後の同じセッションの中では、まだ存在を認識してもらえないんですね。ファイル自体は正しく書けていたのに、タイミングの問題だったわけです。
その場はSKILL.mdに書いた手順を自分で手動になぞって実行し、なんとか処理を通しました。そして次にセッションを開き直したら、/generate-post がちゃんと正式なコマンドとして動いてくれました。「困ったら一度セッションを開き直す」という、地味だけど大事なコツを覚えた1件です。
つまずき一覧・早見表(11件まとめ)
ここまでを、あとから見返しやすいように一覧表にまとめておきます。似た症状に出くわしたとき、索引代わりに使ってもらえたら嬉しいです。
| No | 症状(エラーなど) | 原因の分類 | 解決の一手 |
|---|---|---|---|
| 1 | Node.js/npmが無い | 環境 | Node.jsから導入 |
| 2 | EACCES: permission denied | 環境 | npm prefixをホーム配下に変更 |
| 3 | UnicodeEncodeError → 401 | 認証 | 表示名でなくログイン名を指定 |
| 4 | source .envで「コマンドが見つかりません」 | 環境 | スペースを含む値をクォートで囲む |
| 5 | IneligibleTierError | 認証 | settings.jsonをapi-key指定に修正 |
| 6 | 429 RESOURCE_EXHAUSTED | 認証 | Google AI Studioで課金設定 |
| 7 | SSLError: TLSV1_ALERT | ツール仕様 | requestsをやめcurl呼び出しに切替 |
| 8 | 878件が1クラスタに暴走 | データ | 除外語集合の完全一致で判定 |
| 9 | サジェストに人名・地名混入 | データ | トークン完全一致フィルタ強化 |
| 10 | カテゴリー名の大小1字違い | データ | 正しい名前へ1文字修正 |
| 11 | Unknown command | ツール仕様 | セッションを開き直す |
やってみて分かった「設計のこだわり」
たくさんつまずいた分、この仕組みには自分なりに大事にしたポイントがいくつか宿りました。技術自慢というより、非エンジニアなりに「安心して回せること」を優先した結果のこだわりです。
1つ目は、サブエージェント(Task機能)の活用です。アウトラインや執筆といった中間データをメインの会話にずるずる残さず、サブエージェントの中で完結させることで、トークンの無駄遣いをかなり抑えられました。作業机を都度きれいに片付けるイメージですね。
2つ目は、リサーチをGemini CLIに外出ししたこと。いちばんトークンを食いそうな下調べをClaudeの外で済ませることで、Claudeのトークン消費を実質ゼロに抑えました。適材適所でツールを使い分けるだけで、コスト感がずいぶん変わります。
3つ目と4つ目は、繰り返しになりますが安全運用の話です。投稿は常に下書きにして自動公開はしないこと、そして存在しないカテゴリーは自動作成しないこと。この2つの「あえてやらない」設計が、暴走やゴミデータの増殖を防いでくれました。派手さはありませんが、非エンジニアが安心して任せるための土台だと思っています。
まとめ:「Claude Code WordPress 自動」化に挑んで感じたこと
長い道のりでしたが、こうして振り返ると、非エンジニアの私でも自動投稿パイプラインをちゃんと形にできました。1人だったら途中で投げ出していたはずで、Claude Codeに伴走してもらえたからこそ最後までたどり着けた、というのが正直な感想です。
ただ、きれいごとを言うつもりはありません。環境構築の地雷は、どうやっても避けきれませんでした。Node.jsが無いところから始まり、認証、SSL、データの暴走まで、本当に色々ありました。だからこそ、この「つまずき全記録」が、これから同じ道を歩く誰かの近道になればいいなと心から思っています。
次にやりたいのは、キーワード選定のさらなる高度化です。検索意図をもっと丁寧に汲み取って、書くべきテーマを賢く提案してくれる仕組みにしていけたら面白そうだな、と余韻に浸りながら考えています。
最後に、もし同じような自動投稿の仕組みを試したい方がいたら、下敷きにした参考記事「https://www.m-totsu.com/1508/」と、この記事のつまずき早見表を横に置きながら進めてみてください。きっと、私がハマった落とし穴のいくつかは、あなたなら軽々と飛び越えられるはずです。


