MarkdownからWordPressへ自動投稿!CodexスキルとPythonで作る仕組み

MarkdownからWordPressへ自動投稿!CodexスキルとPythonで作る仕組み

こんにちは、えびかずきです!

今回は、Markdownで書いた原稿をWordPressへアップロードする仕組みを作りました。Codexから繰り返し使えるよう、blog-uploaderというスキルにまとめています。

やりたいのは、本文のコピーだけではありません。ローカルの画像も登録し、一度投稿した記事は同じ原稿から更新できるようにすることです。

そこで、Markdownを原稿の正本にし、Codexが手順を進め、Pythonが変換と通信を担当する構成にしました。この記事では、各部品をどうつなぎ、本文・画像・投稿IDをどう受け渡しているかを紹介します。

Markdownでブログを書いている方や、AIエージェントに任せる操作とプログラムで処理する部分を分けたい方の参考になればうれしいです。

全体像:4つの役割に分ける

まず、今回の構成を図にすると次のようになります。

Markdownで原稿を管理し、CodexスキルからPython CLIを実行してWordPressへ送り、投稿IDを原稿へ保存する構成図

上から下へ投稿処理が進み、WordPressが発行した投稿IDをローカル原稿へ戻します。

担当 役割
Obsidian / Markdown 本文・タイトル・画像指定を管理する
Codex + スキル 投稿前確認、実行、結果確認の手順を進める
Python CLI 原稿の解析、HTML変換、画像登録、API通信を行う
WordPress 投稿とメディアを保存し、公開時には記事を配信する

ポイントは、スキルの説明文だけで投稿しているわけではないことです。SKILL.mdには操作手順を書き、実際の変換や通信はPythonのコードへ任せています。

たとえば「先に送信内容を確認する」はスキルの手順です。一方、「MarkdownをHTMLへ変換する」「画像をAPIへ送る」はPythonの処理です。このように分けると、手順を見直す場所と、実装を直す場所を追いやすくなります。

今回の開発環境はWindows 11、Python 3.12、パッケージ管理はuvです。HTTP通信にhttpx、Markdown変換にmarkdown、原稿冒頭の設定を読むためにpython-frontmatterを使っています。

記事を登録する入口はWordPress REST API

最初は、以前の記憶からFTPでアップロードすることを考えていました。

ところが、原稿ファイルをサーバーへ置くだけでは、WordPressの記事として登録されません。今回扱いたいのは、本文だけでなく、タイトル、下書き状態、アイキャッチなどの記事データです。

そこで、外部プログラムからWordPressを操作する窓口であるREST APIを使いました。認証にはアプリケーションパスワードを使い、HTTPS経由で接続しています。

この構成では、主に2つの入口を使います。

APIの入口 今回の用途
/wp-json/wp/v2/media ローカル画像をメディアライブラリへ登録する
/wp-json/wp/v2/posts 記事を新規作成する
/wp-json/wp/v2/posts/<id> 指定IDの記事を更新・取得する

画像と記事は別々に登録します。画像を送っただけでは、記事のアイキャッチに設定したことにはならない、という点が大事でした。

ローカル原稿に「WordPressとの対応」を持たせる

原稿はMarkdownで保存し、冒頭にタイトルや投稿状態などを記述します。この設定部分がfront matterです。

以下は、新規記事を用意するときの例です。

---
title: MarkdownからWordPressへ投稿してみた
slug: markdown-wordpress-example
status: draft
excerpt: Markdown原稿をWordPressへ送る仕組みの紹介です。
---

新規投稿に成功すると、WordPressが発行したpost_idを原稿へ保存します。アイキャッチを設定した場合は、画像を識別するfeatured_mediaも保存します。

この実装では、post_idがなければ新規作成、あればその投稿を更新するという切り替えにしています。

タイトルが同じかどうかで判断するのではなく、WordPress上の投稿とローカル原稿をIDで結び付けるわけですね。投稿のたびに新しい記事が増えることを防ぎ、原稿を編集して再送する流れにできます。

ただし、投稿の作成後に通信が切れたり、ローカルへのID保存に失敗したりした場合は注意が必要です。結果が分からないまま新規投稿をやり直さず、WordPress側に作成済みの記事がないか確認する手順にしています。

本文・画像・投稿IDは、それぞれ違う流れになる

本文はHTMLへ変換して送る

Python CLIは、front matterとMarkdown本文を分けて読み込みます。その後、本文をHTMLへ変換し、タイトルやslugなどと一緒にWordPressへ送るデータにまとめます。

つまり、WordPressへ送っているのはMarkdownファイルそのものではなく、変換後のHTMLと記事の設定値です。原稿は手元に残るので、編集は引き続きObsidianなどで行えます。

本文画像にはURL、アイキャッチにはIDを使う

Markdownから参照しているローカル画像は、まずMedia APIへ送ります。WordPressから返ってくる画像URLとメディアIDを、用途ごとに使い分けます。

使う場所 投稿へ渡すもの
本文の画像 WordPressが返した画像URL
アイキャッチ featured_mediaに設定するメディアID

同じ画像を本文とアイキャッチに使う場合は、1回の処理内でアップロード結果を再利用するようにしました。同じパスの画像を二度送らないための工夫です。

ただし、これは実行をまたいで全画像の重複を防ぐ仕組みではありません。現在は、本文画像が記事の更新時に再アップロードされる場合があります。

投稿IDは原稿へ戻す

送信して終わりにせず、返されたIDを原稿へ保存することで、次の更新へつなげます。

また、保存だけで完了とはせず、投稿をもう一度取得し、下書き状態やアイキャッチのIDが意図どおりかを確認します。「APIが成功を返したこと」と「目的の設定になったこと」を分けて確認するためです。

投稿前の確認から反映確認まで

通常の処理は次の順番にしました。

  1. 接続と認証を確認する。
  2. dry-runでタイトル、状態、HTML、画像パスを確認する。
  3. 本文に使う画像や、指定されたアイキャッチ画像を登録する。
  4. 新規記事を下書きで作成するか、post_idのある記事を更新する。
  5. 投稿IDなどをローカル原稿へ保存する。
  6. WordPress側の投稿を再取得して反映を確認する。

dry-runは、実際には投稿せず、送信予定の内容を確認するための処理です。今回の実装ではWordPressへの通信を行わず、ローカルで原稿と変換結果を確認できます。認証確認は別の操作として行います。

投稿状態の既定値はdraftにしています。「アップロードして」と頼んだ段階では下書きを作り、公開する場合は明示的な指示を受ける手順です。

認証情報と記事の設定は分けて管理する

接続先や認証情報は、スキル外の~/.codex/.envから読み込む構成にしました。通常のログインパスワードを原稿に書くことはせず、アプリケーションパスワードの値も記事・Git・レビュー記録へ含めません。

原稿から投稿へ渡す設定にも、形式の違いがありました。

Obsidianでは技術/Pythonのような文字列をタグとして使います。一方、今回のWordPress APIへ渡すタグやカテゴリーは数値IDです。そのまま送ると形式が合いません。

そこで、Obsidian用のタグは原稿に残し、WordPressへ送るデータからは除外する処理にしました。WordPress用のタグを使う場合は、確認した数値IDを指定します。名称から自動で検索・作成する機能は、現時点では入れていません。

スキル単体で使えるようにする

最初のスキルは、別の開発プロジェクトにあるプログラムを呼び出していました。しかし、そのプロジェクトを移動・削除すると、スキルも動かなくなってしまいます。

そこで、必要なPythonコード、依存関係を固定するuv.lock、テストをスキルのフォルダー内へまとめました。

blog-uploader/
├─ SKILL.md                 # 操作手順
├─ pyproject.toml           # Pythonの依存設定
├─ uv.lock                  # 依存バージョンの固定
├─ scripts/
│  └─ run_blog_uploader.py  # 実行入口
├─ src/wordpress_manager/
│  ├─ article.py            # 原稿の読込・変換
│  ├─ cli.py                # 投稿処理全体の制御
│  ├─ client.py             # WordPressとの通信
│  └─ config.py             # 認証設定の読込
├─ tests/
└─ references/

ここでいう自己完結とは、旧開発プロジェクトに依存しない、という意味です。Pythonとuv、外部に置いた認証設定、接続できるWordPressは必要になります。

認証情報まで一緒に詰め込むのではなく、実装と依存設定をまとめ、秘密の値は分離しておく構成にしました。

実運用で分かったことを手順へ戻す

実際に動かしてみると、変換や通信以外にも直すところがありました。

たとえばWindowsでは、UTF-8のファイルを既定の文字コードで読もうとして検証が失敗しました。検証時にUTF-8を明示し、実行ラッパーの出力もUTF-8へそろえています。

また、画像フォルダーを指定された際に、既存画像と同じだと判断して更新しなかったことがありました。そのときの報告では、WordPressへ実際に反映したのかが十分明確ではありませんでした。

この経験から、アイキャッチは画像ファイルを明示して扱い、実行していない更新を「変更済み」と報告しないこと、変更後に投稿を再取得して確認することを手順へ加えました。

利用後には、実際のエラーや修正要求、検証結果を一度レビューします。原因が分かった小さな問題はバックアップを取って修正し、再検証します。改善が不要だった場合もno_changeとして記録する形です。

確認できたことと、残っている課題

2026年10月22日の開発記録では、自動テスト16件が成功し、Ruffや依存ロックの検査も通過しています。実サイトでも、認証、下書き作成、本文画像の登録、アイキャッチの設定・差し替えを確認しました。

一方で、次の点は現在の制約です。

  • WordPressのカテゴリー・タグは、数値IDの指定が必要です。
  • 本文画像は更新時に再アップロードされる場合があります。
  • 不要になったメディアは自動削除しません。
  • WordPressテーマによる表示崩れは自動判定できません。

今回作った仕組みでは、Markdownを編集する作業と、WordPressへ反映する処理をつなげられました。そのために重要だったのが、本文だけでなく、画像と投稿IDの流れも決めておくことでした。

特に、投稿IDを原稿へ戻すことで「一度送って終わり」から「同じ原稿を更新し続ける」流れにできたのは、今回の構成の要点です。今後の改善候補としては、本文画像の再利用と、カテゴリー・タグの指定を扱いやすくすることが残っています。

その他カテゴリの最新記事