PythonでWordPress下書きを自動作成する仕組みを作った話|OpenAI APIとWordPress REST APIの実務メモ

AI活用

PythonでWordPress下書きを自動作成する仕組みを作った話|OpenAI APIとWordPress REST APIの実務メモ

WordPressで記事を書いていると、本文を書く作業そのものよりも、投稿画面への入力作業が地味に重いことがあります。

タイトルを入れる。

本文を貼る。

タグを入れる。

カテゴリを選ぶ。

スラッグを確認する。

メタディスクリプションを入れる。

アイキャッチ画像を設定する。

下書き保存する。

1記事だけなら大したことはありません。

しかし、複数サイトを運用したり、記事本数が増えたりすると、この投稿前の事務作業がじわじわ効いてきます。

そこで、OpenAI APIで記事データを生成し、WordPress REST APIでWordPressへ下書き保存するローカルPythonプログラムを作りました。

タイトルでは「自動投稿」と言いたくなりますが、実際には自動公開ではなく、下書き作成までです。

公開ボタンは人間が押します。

ここはかなり意識して線を引きました。


作ったもの

作ったのは、記事ネタからWordPress下書きまでをつなぐローカルPythonプログラムです。

大まかな流れは次のようなものです。

[人間 / ChatGPT]
  記事ネタ・方針を整理
        ↓
[ローカルPython]
  入力を読み込み、記事生成APIへ渡す
        ↓
[OpenAI API]
  記事タイトル・本文・メタ情報などを生成
        ↓
[WordPress REST API]
  WordPressへ下書き保存
        ↓
[人間]
  管理画面で確認・修正・手動公開

やっていること自体は、そこまで複雑ではありません。

  • 記事ネタを用意する
  • PythonがOpenAI APIを呼び出す
  • 生成結果をWordPress投稿用データにする
  • WordPress REST APIで下書き保存する
  • アイキャッチ画像があればメディアにアップロードして設定する
  • 人間が最後に確認して公開する

実際に作ってみると、難しかったのはコードそのものよりも、どこまで自動化しないかでした。


なぜWordPress下書き自動作成を作ったのか

自分は建設コンサル系の実務をやっています。

道路、橋梁、河川、点検、調書、Excel、Word、GIS、電子納品。

そういう泥臭い実務です。

その一方で、WordPressサイトも運営しています。

維持DXノートでは、建設コンサル実務、Excel帳票、Word帳票、AI活用、無料ツール紹介などの記事を書いています。

記事を書くこと自体は必要です。

ただ、毎回のWordPress投稿作業が地味に面倒でした。

特に、次のような作業が積み重なります。

  • 記事タイトルの入力
  • 本文Markdownの貼り付け
  • SEOタイトルの入力
  • メタディスクリプションの入力
  • タグの入力
  • カテゴリの指定
  • スラッグの指定
  • アイキャッチ画像のアップロード
  • 下書き保存
  • 投稿後の確認

そこで考えました。

記事の最終判断は人間がやるとして、WordPress下書き作成まではPythonにやらせればよいのではないか

これが今回の出発点です。


最初に決めたこと:自動公開しない

最初に決めたのは、技術ではなく運用方針です。

POST_STATUS = "draft"
ALLOW_PUBLISH = False
ALLOW_DELETE = False
ALLOW_OVERWRITE_PUBLISHED = False

技術的には、WordPress REST APIで公開状態の投稿を作ることもできます。

しかし、今回はやりませんでした。

AIが生成した記事を、そのまま公開するのは危ないからです。

内容の間違いもあります。

言いすぎもあります。

文脈を外すこともあります。

SEO的にはよさそうでも、実務者としての責任境界を越える表現になることもあります。

だから、Pythonプログラムは下書き保存までにしました。

これは機能不足ではなく、設計判断です。


ChatGPTとClaudeと人間の分業

今回、PythonコードはClaudeにかなり書いてもらいました。

ただし、丸投げはしていません。

自分の使い分けは、だいたい次のような形です。

ChatGPT:
  - やりたいことの整理
  - 処理の流れの整理
  - Claudeに渡す仕様の整理
  - 例外処理方針の整理
  - WordPress投稿ルールの整理

Claude:
  - Python実装
  - エラー修正
  - 差分修正
  - リファクタリング

人間:
  - 目的を決める
  - 自動化しない範囲を決める
  - ローカルで動作確認する
  - WordPress下書きを確認する
  - 最終公開する

ここで一番大事だったのは、Claudeに曖昧な状態で丸投げしないことでした。

Claudeは実装が速いです。

しかし、仕様が曖昧だと普通にブレます。

そのため、先にChatGPT側で、

  • 何をするか
  • 何をしないか
  • どの状態を成功とするか
  • どの状態なら止めるか
  • どの処理は人間に残すか

を整理しました。

そのうえで、Claudeには「この方針で実装して」と渡しました。

AI開発では、コードを書く前の仕様固定がかなり重要です。


OpenAI APIで記事データを作る

OpenAI API側では、記事の材料を渡して、WordPress投稿用のデータを返してもらいます。

イメージとしては、次のような処理です。

def build_article_prompt(common_rule: str, article_seed: dict) -> str:
    return f"""
あなたは日本語のWordPress記事作成担当です。
以下の共通ルールと記事情報に従って、WordPress下書き用の記事データを作成してください。

# 共通ルール
{common_rule}

# 記事情報
{article_seed}

# 出力してほしい項目
- title
- body_markdown
- slug
- meta_description
- tags
- category
- eyecatch_prompt
"""

ここで重要なのは、ChatGPT画面のスレッド文脈はOpenAI APIに自動では引き継がれないということです。

ChatGPTでどれだけ記事方針を議論していても、APIに渡さなければ、API側のモデルはその文脈を知りません。

そのため、毎回必要な共通ルールと記事情報を、明示的にAPIへ渡す形にしました。

これは、記事生成APIを使うときにかなり大事なポイントです。


WordPress REST APIで下書きを作る

WordPress側はREST APIを使いました。

下書き作成だけなら、考え方はかなりシンプルです。

import requests
from requests.auth import HTTPBasicAuth

def create_wordpress_draft(
    base_url: str,
    username: str,
    app_password: str,
    title: str,
    content: str,
    slug: str,
    category_ids: list[int],
    tag_ids: list[int],
    featured_media_id: int | None = None,
) -> dict:
    endpoint = f"{base_url.rstrip('/')}/wp-json/wp/v2/posts"

    payload = {
        "title": title,
        "content": content,
        "status": "draft",
        "slug": slug,
        "categories": category_ids,
        "tags": tag_ids,
        "comment_status": "closed",
        "ping_status": "closed",
    }

    if featured_media_id:
        payload["featured_media"] = featured_media_id

    response = requests.post(
        endpoint,
        json=payload,
        auth=HTTPBasicAuth(username, app_password),
        timeout=30,
    )

    response.raise_for_status()
    return response.json()

実際には、この周辺にもう少し処理が必要です。

  • カテゴリ名からIDを取得する
  • タグがなければ作成する
  • 同じslugがないか確認する
  • アイキャッチ画像をアップロードする
  • エラー時にログを残す
  • 処理済みの記事を記録する

ただ、全部を公開記事に細かく書きすぎると、内製ツールの中身を出しすぎます。

この記事では、考え方が伝わる範囲に留めます。


WordPress Application Passwordを使う

WordPressの通常ログインパスワードをPythonに持たせるのは避けました。

今回は、WordPressのApplication Passwordを使っています。

.env は次のようなイメージです。

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx

WP_BASE_URL=https://example.com
WP_USERNAME=example_user
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx

MAX_ITEMS_PER_RUN=3

実際の値は当然公開しません。

ローカルツールでも、.env はGit管理から外します。

.env
logs/
generated/

このへんは普通の話ですが、自分用ツールほど雑になりがちです。

最初に分けておく方が安全です。


カテゴリIDを取得する

WordPress REST APIで投稿する場合、カテゴリは名前ではなくIDで渡します。

そのため、カテゴリ名からIDを取得する処理が必要です。

単純化すると次のような処理です。

def get_category_id(base_url: str, auth, category_name: str) -> int:
    endpoint = f"{base_url.rstrip('/')}/wp-json/wp/v2/categories"

    response = requests.get(
        endpoint,
        params={"search": category_name},
        auth=auth,
        timeout=30,
    )
    response.raise_for_status()

    categories = response.json()

    for category in categories:
        if category.get("name") == category_name:
            return category["id"]

    raise ValueError(f"Category not found: {category_name}")

カテゴリについては、自動作成しない運用にしました。

カテゴリはサイト構造に関わるため、Pythonが勝手に増やすと後で整理が面倒になるからです。


タグは必要に応じて作る

一方で、タグは記事単位で増えても、カテゴリほど致命的にはなりにくいです。

そこで、タグは存在しなければ作成する運用にしました。

def get_or_create_tag_id(base_url: str, auth, tag_name: str) -> int:
    endpoint = f"{base_url.rstrip('/')}/wp-json/wp/v2/tags"

    search_response = requests.get(
        endpoint,
        params={"search": tag_name},
        auth=auth,
        timeout=30,
    )
    search_response.raise_for_status()

    for tag in search_response.json():
        if tag.get("name") == tag_name:
            return tag["id"]

    create_response = requests.post(
        endpoint,
        json={"name": tag_name},
        auth=auth,
        timeout=30,
    )
    create_response.raise_for_status()

    return create_response.json()["id"]

このあたりは地味ですが、WordPress自動下書き作成では避けて通れません。

本文だけ生成できても、カテゴリやタグで止まると実務では使いにくいです。


アイキャッチ画像をアップロードする

アイキャッチ画像は、Python側で生成するのではなく、人間が別途用意する運用にしました。

Python側では、画像ファイルがあればWordPressのメディアライブラリへアップロードし、投稿の featured_media に設定します。

from pathlib import Path

def upload_media(
    base_url: str,
    auth,
    image_path: Path,
) -> int:
    endpoint = f"{base_url.rstrip('/')}/wp-json/wp/v2/media"

    headers = {
        "Content-Disposition": f'attachment; filename="{image_path.name}"',
        "Content-Type": "image/png",
    }

    with image_path.open("rb") as f:
        response = requests.post(
            endpoint,
            headers=headers,
            data=f,
            auth=auth,
            timeout=60,
        )

    response.raise_for_status()
    return response.json()["id"]

取得した media_id を、投稿作成時の featured_media に渡します。

media_id = upload_media(base_url, auth, image_path)

post = create_wordpress_draft(
    base_url=base_url,
    username=username,
    app_password=app_password,
    title=title,
    content=content,
    slug=slug,
    category_ids=category_ids,
    tag_ids=tag_ids,
    featured_media_id=media_id,
)

これで、WordPress管理画面上では、アイキャッチつきの下書きとして確認できます。


複数サイト対応

複数のWordPressサイトを扱いたかったため、投稿先サイトを切り替えられるようにしました。

考え方は単純です。

def get_site_config(site_key: str, env: dict) -> dict:
    prefix = site_key.upper()

    return {
        "base_url": env[f"{prefix}_WP_BASE_URL"],
        "username": env[f"{prefix}_WP_USERNAME"],
        "app_password": env[f"{prefix}_WP_APP_PASSWORD"],
    }

.env 側は次のようなイメージです。

SITE_A_WP_BASE_URL=https://site-a.example.com
SITE_A_WP_USERNAME=user_a
SITE_A_WP_APP_PASSWORD=xxxx xxxx xxxx xxxx

SITE_B_WP_BASE_URL=https://site-b.example.com
SITE_B_WP_USERNAME=user_b
SITE_B_WP_APP_PASSWORD=yyyy yyyy yyyy yyyy

実際には、記事ごとにどのサイトへ投げるかを持たせています。

ここまでやると、複数サイト運用での下書き作成がかなり軽くなります。


処理件数を制御する

一度に何十件も下書きを作ると、確認が大変です。

そこで、1回の実行件数を .env で制御できるようにしました。

def get_max_items_per_run(env: dict, default: int = 1) -> int:
    value = env.get("MAX_ITEMS_PER_RUN", str(default))

    try:
        max_items = int(value)
    except ValueError:
        return default

    return max(1, max_items)

使う側は次のようなイメージです。

max_items = get_max_items_per_run(env, default=1)

for article in pending_articles[:max_items]:
    process_article(article)

最初から10件、20件と走らせるより、まずは1件、次に3件くらいで試す方が安全です。

自動化は、成功するとつい件数を増やしたくなります。

でも、WordPressの下書きが大量にできても、最後に読むのは人間です。

ここはかなり大事です。


エラーは止めずにログへ逃がす

こういう自動化で一番困るのは、1件失敗しただけで全部止まることです。

そのため、基本方針は、止めるよりログへ逃がすことにしました。

def run_batch(articles: list[dict]) -> None:
    for article in articles:
        try:
            process_article(article)
            write_log(article, result="success")
        except RecoverableArticleError as e:
            write_log(article, result="failed", message=str(e))
            continue
        except FatalEnvironmentError:
            raise

全部完璧に止めるより、どこで何が起きたかを残し、後で直せるようにする。

この考え方は、建設コンサル実務にも近いです。

成果品でも、エラーが出た瞬間に全体を破壊するより、どこが不整合なのか記録して潰していく方が現実的です。


重複投稿を避ける

自動化で怖いのは、同じ記事を何度も作ってしまうことです。

そこで、投稿前にslugの重複を確認します。

def wordpress_slug_exists(base_url: str, auth, slug: str) -> bool:
    endpoint = f"{base_url.rstrip('/')}/wp-json/wp/v2/posts"

    response = requests.get(
        endpoint,
        params={"slug": slug, "status": "any"},
        auth=auth,
        timeout=30,
    )
    response.raise_for_status()

    return len(response.json()) > 0

使う側は次のようなイメージです。

if wordpress_slug_exists(base_url, auth, slug):
    raise RecoverableArticleError(f"Slug already exists: {slug}")

自動投稿系は、うまくいった時より、失敗した時の後始末が面倒です。

重複下書きが増えると、人間が確認するコストが上がります。

そのため、slug重複確認は最初から入れました。


実際に動かしてみた

一通りの処理フローを組み上げて、ローカル環境で動作確認を行いました。

流れとしては、次のところまで通っています。

記事データ生成
↓
ローカル保存
↓
WordPressカテゴリ解決
↓
WordPressタグ作成
↓
アイキャッチ画像アップロード
↓
WordPress下書き作成
↓
投稿ID取得
↓
処理結果を記録

WordPress管理画面に、アイキャッチつきの下書きが作成されたのを確認しました。

この瞬間は普通にうれしかったです。

建設業界のおじさん、年度初めにPythonで記事作成の内製ラインを立ち上げました。


一般公開しない理由

ここまで作ると、少し考えます。

これを公開したら、誰か使うのではないか。

ただ、すぐにやめました。

理由は明確です。

公開すると、たぶんサポートが本体になる

想定される詰まりどころはいくらでもあります。

  • OpenAI APIキーの取得
  • WordPress Application Passwordの発行
  • WordPress REST APIの権限
  • サーバー設定
  • SSL
  • カテゴリ・タグの扱い
  • 画像アップロード権限
  • テーマやプラグイン差分
  • API仕様変更
  • 生成結果の品質確認

自分用ローカルツールなら強いです。

しかし、一般公開すると、たぶん「APIキーって何ですか」「WordPressに投稿できません」「カテゴリが作れません」の対応が本体になります。

やるならWebアプリ化やSaaS化が必要です。

しかし、それはまた別の事業です。

今回は、公開ツールではなく、内製ラインとして使うことにしました。


コードを書くより、境界線を決める方が大事だった

今回やって思ったのは、AI時代の開発では、コードを書く力よりも、どこまで機械にやらせるかを決める力の方が大事だということです。

Claudeはかなり実装してくれます。

ただし、仕様が曖昧だと普通に迷います。

逆に、方針が固まっていると速いです。

今回効いたのは、この順番でした。

1. ChatGPTでやりたいことを整理する
2. 自動化する範囲としない範囲を決める
3. Claudeに実装させる
4. ローカルで動かす
5. エラーだけClaudeに返す
6. 差分修正する

AIに丸投げするのではなく、AIに投げる前の境界線を決める。

これが一番効きました。


自動化できることを、あえて自動化しない

今回の一番のポイントは、ここだと思っています。

  • 記事生成は自動化する
  • WordPress下書き保存も自動化する
  • アイキャッチ設定も自動化する
  • でも公開は自動化しない
  • 公開済み記事の上書きもしない
  • 削除もしない

この線引きが大事でした。

全部自動化できるから全部やる、ではありません。

自動化すると危ないところは、人間に残す。

この設計にしたことで、実務で使える安心感がかなり上がりました。


WordPress自動投稿ではなく、WordPress下書き自動作成

今回の仕組みは、厳密にはWordPress自動投稿ツールではありません。

WordPress下書き自動作成ツールです。

ここは、あえて言葉を分けたいところです。

自動投稿という言葉は便利ですが、公開まで自動化すると責任が重くなります。

特に、AI生成記事では、

  • 事実確認
  • 表現の強さ
  • SEO狙いの過剰表現
  • 引用やリンク
  • 読者に与える印象
  • サイト全体の方針との整合

を人間が確認する必要があります。

だから、Pythonには下書きまでを任せる。

公開判断は人間が行う。

このくらいの距離感が、今の自分にはちょうどよいです。


まとめ

Pythonで、OpenAI APIとWordPress REST APIをつなぎ、WordPress下書きを自動作成するローカルプログラムを作りました。

作ってみて感じたことは次のとおりです。

  • OpenAI APIとWordPress REST APIをつなぐこと自体は難しすぎるわけではない
  • 難しいのは運用設計
  • 自動公開しない判断はかなり大事
  • ChatGPTは方針整理に向いている
  • Claudeは実装と差分修正に向いている
  • WordPress Application Passwordを使うと運用しやすい
  • カテゴリ・タグ・slug・アイキャッチ処理が地味に重要
  • 自分用ローカルツールとしてはかなり強い
  • 作れることと、一般公開できることは違う

WordPress自動投稿というと派手に聞こえます。

でも実務的には、WordPress下書き作成までを自動化して、公開は人間が押すくらいがかなり現実的です。

建設業界のおじさんでも、年度初めの空白時間とAIがあれば、記事作成の内製ラインは組めます。

ただし、公開ボタンだけは人間が押す。

ここは、今後もしばらく譲らないと思います。


関連ツール・相談について

今回紹介した仕組みは、現時点では自分用のローカル運用として作成したものです。

そのため、このPythonプログラム一式をそのまま一般配布する形にはしていません。

理由は、WordPress REST API、OpenAI API、Application Password、カテゴリ・タグ設定、アイキャッチ画像アップロードなど、利用環境ごとの差が大きいためです。

ただし、維持DXでは、Excel・Word帳票、AI活用、WordPress下書き作成、投稿前整理などに関する小さな業務改善ツールや実務メモを公開しています。

関連ツールのダウンロードや、WordPress下書き作成パイプライン、AI記事生成フロー、業務用WordPress運用の相談をご希望の方は、以下のフォームからお問い合わせください。

    お名前(任意)

    メールアドレス(必須)

    会社名・所属(任意)

    ご関心のある内容(任意)

    ご相談内容(任意)

    ※公開中の無料ツールがある場合は、フォーム送信後にダウンロード案内をお送りします。 ※個別環境への導入支援やカスタマイズは、内容を確認したうえで個別対応となります。

    維持DXノートについて

    維持DXノートでは、建設コンサル実務、Excel帳票、Word帳票、AI活用、PythonやVBAによる小さな業務改善の記録を公開しています。

    今回のWordPress下書き自動作成も、単なるプログラミングではなく、実務者が自分の発信作業を軽くするための内製ラインとして作ったものです。

    AIや自動化を使う場合でも、最後の確認と公開判断は人間に残す。

    このくらいの距離感で、今後も小さな実務改善を積み上げていきます。

    関連ツールのダウンロードや、業務改善ツールの相談をご希望の方は、以下のフォームからお問い合わせください。

      お名前(任意)

      メールアドレス(必須)

      会社名・所属(任意)

      ご関心のある内容(任意)

      ご相談内容(任意)