WordPress自動投稿ツールを全公開|Codexに頼むだけで下書き保存

こんにちは、のた(@Nor21011)です。

書いたブログ記事を、Codexに頼むだけでWordPressへ下書き投稿してくれる。そんなツールを自分で作りました。

この記事では、そのツールの中身を、コードも指示書もまとめて全部公開します。コードが読めなくても大丈夫です。むしろ、コードは読めないけれど「AIに自分専用のツールを作ってもらう流れ」を知りたい人に向けて書いています。

難しい話は極力省いて、何をどうしているのかを順番にご紹介していきます。

このツールはCodexに作ってもらいました。とはいえ、すんなり完成したわけではありません。自動化を頼んだはずが、いつのまにか自分が実行係になって4時間溶かす、という遠回りもしています。

どうやってこのツールにたどり着いたのか、その悪戦苦闘のほうは前の記事にまとめてあります。

自動化したかっただけなのに、気づけば自分が実行係になっていた|Codexで4時間溶かしてわかったAIへの任せ方

今回はその続編として、完成したツールの現物をすべてご紹介します。持ち帰ってほしいのは、次の3つです。

  • post_to_wp.py:動かすコード
  • AGENTS.md:AIにやらせないことを決める指示書
  • wp-md-format.md:投稿用ファイルの作り方ルール

この3ファイルさえあれば、あとはAIに相談しながら自分の環境に合わせていけます。

目次

このツールでできること

普段、私は自分のPCで記事を書き、その内容をWordPressに転記して投稿しています。具体的には、タイトルやスラッグ、本文などを一つずつ管理画面に貼り付けていく作業です。この転記がツール導入でどう変わるのかを比較したものが、下の表です。

  手作業のころ ツール導入後
全体の手間 項目を1つずつ管理画面に貼り付け ✓ Codexに頼むだけ
タイトル 手作業で転記 ✓ 自動で入る
スラッグ 手作業で転記 ✓ 自動で入る
メタディスクリプション 手作業で転記 ✓ 自動で入る
カテゴリ 手で選択 ✓ 自動で設定される
タグ 手作業で転記 ✓ 自動で入る(なければ作成)
本文 カスタムHTMLブロックに貼り付け ✓ 自動で入る
保存 手動で下書き保存 ✓ 自動で下書き保存まで完了

これまで手作業でやっていた転記が、Codexに頼むだけで終わるようになりました。

結果として手に入った価値は、3つあります。

  • 待っている間に別の作業ができる。自分が拘束されない
  • 「どこを探して、どこに貼るか」を判断しなくていい。脳のリソースも手間も使わない
  • コピペミスのようなヒューマンエラーが起きない

「速くなった」というより、「自分がやらなくてよくなった」というほうが、感覚としては近いです。時短ツールではなく、自分の手から作業を外す道具、というイメージで作っています。動機の細かいところは前の記事に書いてあるので、ここでは深掘りしません。

ツールが動く仕組み

自分がやること ツールが自動でやること
STEP 1 投稿用ファイルを用意する
STEP 2 Codexに「これを投稿して」と頼む
STEP 3 中身を読み取る
STEP 4 WordPressに下書き投稿する
STEP 5 編集画面のリンクが表示される

投稿用のファイルを用意して、Codexに「これを投稿して」と頼む。ツールが中身を読み取って、WordPressに下書き投稿してくれて、最後に編集画面のリンクが表示される。やっていることは、これだけです。

ここで言う「投稿用ファイル」とは、ひとつの記事に必要な情報を、決まった形にまとめて書いたファイルのことです。

  • ファイル名のルール:必ず _wp.md で終わる名前にしておく(このルールでツールが対象を見つけられる)
  • 中身:タイトル、スラッグ、メタディスクリプション、カテゴリ、タグ、本文を、決まった型に流し込んで書く

骨組みはこんな形です。本文の中身は省略していますが、雰囲気だけ掴んでいただければ大丈夫です。

| **タイトル** | サンプル記事のタイトル |
| **スラッグ** | sample-article |
| **メタディスクリプション** | この記事ではサンプルとして〜 |
| **カテゴリ** | ブログ×AI |
| **タグ** | Claude / AIエージェント |

<!-- WP_BODY_HTML_START -->
ここに本文HTML
<!-- WP_BODY_HTML_END -->

機械が読める決まった型に、必要な情報を流し込むイメージです。

この投稿用ファイルも、決まった形さえ伝えればAIに作ってもらえます。手書きで頑張る必要はありません。私の場合は、ブログ本文を書き終えたタイミングで、Claudeに投稿用ファイルもまとめて作ってもらっています。

作り方のルールは、記事末尾の付録に置いてある wp-md-format.md にまとまっています。AIに「このルールに沿って投稿用ファイルを作って」と渡せば、形は揃います。post_to_wp.pyAGENTS.md もあわせて渡せば、投稿用ファイルの形まで含めて理解してくれます。

実際にCodexに頼んだときの流れを再現したものを置いておきます。「このフォルダの _wp.md を投稿して」とお願いすると、対象ファイルを確認したうえで、タイトル・スラッグ・カテゴリ・タグ・抜粋を見せてくれます。こちらが「yes」と返すと、下書き投稿が完了し、編集URLが返ってきます。画面や返答は、使うAIや指示の仕方によって多少変わります。

articles/drafts
ここにある _wp.md ファイルを WordPress に下書きで更新してください。
⏱ 28秒 作業しました
投稿対象は1件でした。

対象ファイル:
articles/drafts/sample-article_wp.md

投稿前の確認情報です。

タイトル: サンプル記事のタイトル
スラッグ: sample-article
カテゴリ: ブログ×AI
タグ: Claude / AIエージェント
抜粋: この記事ではサンプルとして〜

この内容で WordPress に下書き投稿してよければ、「yes」と返してください。
yes
⏱ 46秒 作業しました
下書き投稿が完了しました。

編集URL: https://example.com/wp-admin/post.php?post=123&action=edit

投稿IDは 123 です。カテゴリ・タグ・抜粋も設定されています。新規タグとして AIエージェント が作成されました。
※ 画面や返答は、使うAIや指示の仕方によって多少変わります。

コードの中身(post_to_wp.py)

コード全文だけ欲しい方は、記事末尾の付録にまとめてあります。

正直なところ、私はこのコードを単体ではほとんど読めません。それでも自分のツールとして扱えているのは、中身を「どんな係がいるか」という形で整理しているからです。

細かい書き方は追わなくて大丈夫です。ここでは、このツールの中にどんな係がいるのかだけを見ていきます。post_to_wp.py は、大きく4つの役に分かれています。

全体を進行する main(まとめ役)
下の3つを順番に呼び出し、確認表示・yes / noの受付・結果報告をする
1
ファイルを読む係
投稿用ファイル(_wp.md)を開いて、文字として読み込む
2
中身を取り出す係
読み込んだ文字から、タイトル・スラッグ・タグ・本文などを抜き出す
3
WordPressとやりとりする係
WordPressに接続して、カテゴリ・タグを調べ、下書きを投稿する

「ファイルを読む係」は、投稿用ファイル(_wp.md)を開いて、文字として読み込みます。

「中身を取り出す係」は、読み込んだ文字から、タイトル・スラッグ・タグ・本文などを抜き出します。本文は、決まったマーカー(WP_BODY_HTML_STARTWP_BODY_HTML_END)の間にあるHTMLだけを取り出すようにしています。マーカーの外に何が書いてあっても拾わないので、メモを混ぜても投稿には影響しません。

「WordPressとやりとりする係」は、WordPressに接続して、カテゴリ・タグを調べ、下書きを投稿します。同じ名前のタグがなければ自動で作りますが、カテゴリは勝手に作らない設計にしています。意図しないカテゴリが増えてしまうのを防ぐためです。

そして、全体を進行する main が、この3つを順番に呼び出して、確認表示や「yes / no」の受付、結果報告までやってくれます。

このコードには、安全のための工夫もいくつか入れてあります。

1つ目は、パスワードのような秘密情報を別ファイルにまとめて、作業フォルダの外(ホームフォルダの直下)に置いていることです。コードと同じフォルダに置いておくと、うっかり記事と一緒に公開してしまう恐れがあります。

2つ目は、AIに対して「その秘密情報を見せない・開かせない」というルールを、別の指示書(AGENTS.md)で課していることです。これは次のセクションで詳しく書きます。

3つ目は、通信を安全にするための設定(https)が満たされていないと、ツール自体が動かないようにしてあることです。これは前の記事でClaudeに指摘されて気づいた話の、現物にあたります。

勝手をさせない指示書(AGENTS.md)

Codexにコードを動かしてもらうには、ただ「動かして」とお願いするだけでは少し怖いところがあります。コードのあるフォルダ全体をのぞけるわけで、関係ないファイルまで読みに行ったり、意図しない操作をされたりすると困ります。

そこで、AIに対して「やっていいこと」「やってはいけないこと」をはっきり伝える指示書を一緒に置いてあります。それが AGENTS.md です。

中身は、たとえばこんなことを書いています。

  • 秘密情報のファイルは絶対に表示しない・開かない・中身を出力しない
  • 投稿ステータスは必ず下書きにする。公開状態にはしない
  • 対象フォルダの中に _wp.md が2件以上あったら、勝手に投稿せず、候補を見せてユーザーに確認する
  • WordPressと通信するエラーが出ても、応答そのものを長文で画面に出さない(秘密情報がまぎれて出ないようにするため)

何をさせるかではなく、何をさせないか。そこを言葉にしておくのが、この指示書の役割です。前の記事で「パスワードが丸見え」だと気づいた話の続きで、Codexに実行係として戻ってもらうための、具体的な安全装置にあたります。全文は記事末尾の付録に置いてあるので、流れだけ追えるはずです。

今の運用スタイル

このツールを作ってから、私は毎記事これでWordPressへ投稿しています。

正直に言うと、時短にはなっていません。Codexが動いている時間も、自分で確認する時間も、足し算するとそこそこかかります。それでも私の中での価値は、はっきり変わりました。

感覚としては、「完全に任せられる外注先ができた」に近いです。前の記事で4時間かけて任せ方を学んだ相手が、今は信頼できる外注先になった、というイメージです。

具体的に変わったのは、次の4つです。

  • Codexが投稿作業をしている間に、別のことを進められる
  • スキマ時間に「これ投稿しておいて」と指示だけ出しておける
  • ファイルを開いてコピペするだけの「考えない作業」から解放された
  • 転記でうっかりミスをしていないか、という確認の不安がなくなった

プレビュー確認はWordPress側で自分の目で見ています。下書き投稿までを任せて、人間が見るべき最後の確認だけ自分でやる、という分担です。

やりたいことはたくさんあるのに、時間は有限です。だからこそ、自分がやらなくていいことは決めて、外に出していく。その一つひとつの積み重ねが、自分が本当にやりたいこと、やるべきことに集中する時間を作ってくれるのだと思っています。

このコードは叩き台

ここまで読んで「自分も同じことをやってみたい」と思った方へ、ひとつだけ先に書いておきます。

これから紹介するコードや指示書は、私の環境では問題なく動いています。ただ、WordPressの環境やテーマ、投稿用ファイルの作り方は人によって違います。なので、読者の方にとっては調整前提の叩き台です。コピーしてそのまま実行しても、おそらく動きません。

ただ、それでまったく問題ありません。動かないコードでも、自分が使っているAIに「このコードを自分の環境で動くように直してほしい」と相談すれば、必要な書き換えはAIがやってくれます。コードの中身が分からなくても、斜め読みできる程度で大丈夫です。

私がやったのは、コードを全部理解することではありません。「こう動いてほしい」「ここは怖いから触らせたくない」「投稿は下書きで止めたい」。そういう要望をAIに伝えて、使える形になるまで一緒に詰めていったことです。

昔の私なら、自分専用の投稿ツールを作るなんて発想すら持っていませんでした。コードが書けないのだから、選択肢にすら入っていなかったと思います。それが、AIに相談しながらなら形にできてしまう。今まで自分にはできなかったことが、できるようになる。私がAIに一番面白さを感じているのは、こういうところです。

コードが読めない私でも、ひとつ自動化できた。これは小さいけれど確かな成功体験でした。読んでくださっているあなたも、まずは一つ作ってみると、その感覚がつかめるはずです。

まずは全部を理解しようとしなくて大丈夫です。3つのファイルをAIに渡して、「自分のWordPress環境で使えるようにしたい」と相談する。最初の一歩は、それで十分です。次に全文を置いておきます。

なお、このツールはWordPressの認証情報を扱います。試すときは必ず下書き投稿のままで確認し、アプリケーションパスワードなどの秘密情報をAIの画面に貼り付けないようにしてください。今回のツールで AGENTS.md に「見せない・開かない」ルールを書いているのは、まさにこのためです。

まとめ

書いたブログ記事を、Codexに頼むだけでWordPressに下書き投稿してくれるツールを、現物ベースでご紹介してきました。

最後に、ひとつだけ視野を広げておきます。今回は実行役にCodexを使っていますが、これはCodexでなくても構いません。コードを読んで、必要なら直して、実行してくれるAIなら、Claude Codeでも同じことができます。

大事なのは、ツールそのもの、つまり投稿用ファイル(資料)とコードのほうです。そこさえ整っていれば、実行するAIは各自が使い慣れているもので構いません。自分の環境に合うように、ぜひカスタマイズしてみてください。

ここから先は、コード・指示書・投稿用ファイルの作り方ルール、3点の全文を載せておきます。持ち帰り用です。

付録:コード全文(持ち帰り用)

3ファイルを順番に載せておきます。コピーして、ご自分のAIに「自分の環境で動くように直して」と渡せば、たいてい何とかなります。

post_to_wp.py

import os
import re
import sys
import requests
from dotenv import load_dotenv

# ホーム直下の秘密情報ファイルを読み込む
ENV_PATH = os.path.expanduser("~/.wp-auto-post.env")
load_dotenv(ENV_PATH)

WP_SITE_URL = os.getenv("WP_SITE_URL")
WP_USERNAME = os.getenv("WP_USERNAME")
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD")


def validate_env():
    """必要な環境変数が設定されているか確認する"""
    missing = []

    if not WP_SITE_URL:
        missing.append("WP_SITE_URL")
    if not WP_USERNAME:
        missing.append("WP_USERNAME")
    if not WP_APP_PASSWORD:
        missing.append("WP_APP_PASSWORD")

    if missing:
        raise ValueError(
            "環境変数ファイルに必要な情報がありません。\n"
            f"読み込み先: {ENV_PATH}\n"
            "不足: " + ", ".join(missing)
        )

    if not WP_SITE_URL.startswith("https://"):
        raise ValueError(
            f"WP_SITE_URL は https:// で始まる必要があります。\n"
            f"現在の値: {WP_SITE_URL}\n"
            "http:// だとアプリケーションパスワードが平文で送信されます。"
        )


def clean_file_path(file_path):
    """Macのドラッグ&ドロップ時につくクォートや空白を除去する"""
    return file_path.strip().strip("'").strip('"')


def read_text_file(file_path):
    """UTF-8でファイルを読む"""
    file_path = clean_file_path(file_path)

    if not os.path.exists(file_path):
        raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")

    with open(file_path, "r", encoding="utf-8") as f:
        return f.read()


def extract_meta_table(text):
    """
    _wp.mdのメタ情報テーブルから値を抽出する。

    想定形式:
    | **タイトル** | xxx |
    | **スラッグ** | xxx |
    | **メタディスクリプション** | xxx |
    | **カテゴリ** | xxx |
    | **タグ** | xxx |
    """
    meta = {}
    pattern = r"^\|\s*\*\*(.*?)\*\*\s*\|\s*(.*?)\s*\|$"

    for line in text.splitlines():
        match = re.match(pattern, line.strip())
        if match:
            key = match.group(1).strip()
            value = match.group(2).strip()
            meta[key] = value

    return meta


def extract_body_html_between_markers(text):
    """
    _wp.md内の本文HTMLマーカーから本文HTMLだけを抽出する。

    想定形式:
    <!-- WP_BODY_HTML_START -->
    ここに本文HTML
    <!-- WP_BODY_HTML_END -->
    """
    start_marker = "<!-- WP_BODY_HTML_START -->"
    end_marker = "<!-- WP_BODY_HTML_END -->"

    pattern = rf"{re.escape(start_marker)}\s*\n?(.*?)\n?\s*{re.escape(end_marker)}"
    match = re.search(pattern, text, re.DOTALL)

    if not match:
        raise ValueError(
            "本文HTMLマーカーが見つかりませんでした。\n"
            "以下の形式で本文HTMLを囲んでください。\n\n"
            "<!-- WP_BODY_HTML_START -->\n"
            "<p>本文HTML</p>\n"
            "<!-- WP_BODY_HTML_END -->"
        )

    html = match.group(1).strip()

    if not html:
        raise ValueError(
            "本文HTMLマーカー内が空です。\n"
            "<!-- WP_BODY_HTML_START --> と <!-- WP_BODY_HTML_END --> の間に本文HTMLを入れてください。"
        )

    return html


def wrap_as_custom_html_block(content_html):
    """
    WordPressのカスタムHTMLブロック形式に包む。
    SWELLのHTML構造を壊さないため、本文HTML自体は加工しない。
    """
    return f"""<!-- wp:html -->
{content_html}
<!-- /wp:html -->"""


def split_tags(tag_text):
    """
    _wp.mdのタグ文字列を分割する。
    想定:Claude Cowork / Claude / AIエージェント
    """
    if not tag_text:
        return []

    tags = re.split(r"\s*/\s*|\s*/\s*|\s*,\s*|、", tag_text)

    return [tag.strip() for tag in tags if tag.strip()]


def wp_request(method, endpoint, **kwargs):
    """WordPress REST APIへの共通リクエスト"""
    validate_env()

    url = f"{WP_SITE_URL.rstrip('/')}/wp-json/wp/v2/{endpoint.lstrip('/')}"

    response = requests.request(
        method,
        url,
        auth=(WP_USERNAME, WP_APP_PASSWORD),
        timeout=30,
        **kwargs,
    )

    if response.status_code not in (200, 201):
        raise RuntimeError(
            "WordPress APIリクエストに失敗しました。\n"
            f"Endpoint: {endpoint}\n"
            f"Status: {response.status_code}\n"
            f"Response: {response.text[:500]}"
        )

    return response.json()


def find_category_id_by_name(category_name):
    """
    WordPress上のカテゴリ名からIDを取得する。
    見つからなければNoneを返す。
    カテゴリは自動作成しない。
    """
    if not category_name:
        return None

    categories = wp_request(
        "GET",
        "categories",
        params={
            "search": category_name,
            "per_page": 100,
        },
    )

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

    return None


def find_tag_id_by_name(tag_name):
    """WordPress上のタグ名からIDを取得する。見つからなければNone。"""
    if not tag_name:
        return None

    tags = wp_request(
        "GET",
        "tags",
        params={
            "search": tag_name,
            "per_page": 100,
        },
    )

    for tag in tags:
        if tag.get("name") == tag_name:
            return tag.get("id")

    return None


def create_tag(tag_name):
    """WordPressにタグを作成してIDを返す"""
    result = wp_request(
        "POST",
        "tags",
        json={
            "name": tag_name,
        },
    )

    return result.get("id")


def get_or_create_tag_ids(tag_names):
    """
    タグ名リストからタグIDリストを作る。
    既存タグがあれば使い、なければ自動作成する。
    """
    tag_ids = []
    created_tags = []

    for tag_name in tag_names:
        existing_id = find_tag_id_by_name(tag_name)

        if existing_id:
            tag_ids.append(existing_id)
        else:
            new_id = create_tag(tag_name)
            tag_ids.append(new_id)
            created_tags.append(tag_name)

    return tag_ids, created_tags


def build_article_from_wp_md(file_path):
    """_wp.mdから投稿用データを作る"""
    text = read_text_file(file_path)

    meta = extract_meta_table(text)
    raw_html = extract_body_html_between_markers(text)

    title = meta.get("タイトル")
    slug = meta.get("スラッグ")
    description = meta.get("メタディスクリプション")
    category_name = meta.get("カテゴリ")
    tag_text = meta.get("タグ")

    if not title:
        raise ValueError("メタ情報に「タイトル」がありません。")

    if not slug:
        raise ValueError("メタ情報に「スラッグ」がありません。")

    tag_names = split_tags(tag_text)
    content = wrap_as_custom_html_block(raw_html)

    return {
        "title": title,
        "slug": slug,
        "description": description,
        "category_name": category_name,
        "tag_text": tag_text,
        "tag_names": tag_names,
        "raw_html": raw_html,
        "content": content,
    }


def save_debug_files(article):
    """
    確認用ファイルを保存する。
    ローカル確認用。GitHubには上げない。
    保存先: ~/wp-auto-post-debug/
    """
    debug_dir = os.path.expanduser("~/wp-auto-post-debug")
    os.makedirs(debug_dir, exist_ok=True)

    raw_html_path = os.path.join(debug_dir, "debug_raw_html.html")
    wp_content_path = os.path.join(debug_dir, "debug_wp_content.html")

    with open(raw_html_path, "w", encoding="utf-8") as f:
        f.write(article["raw_html"])

    with open(wp_content_path, "w", encoding="utf-8") as f:
        f.write(article["content"])

    return raw_html_path, wp_content_path


def create_draft_post(article):
    """WordPressに下書き投稿を作成する"""
    validate_env()

    category_id = find_category_id_by_name(article.get("category_name"))
    tag_ids, created_tags = get_or_create_tag_ids(article.get("tag_names", []))

    payload = {
        "title": article["title"],
        "slug": article["slug"],
        "content": article["content"],
        "status": "draft",
    }

    if article.get("description"):
        # WordPress標準の「抜粋」欄へ入れる
        payload["excerpt"] = article["description"]

    if category_id:
        payload["categories"] = [category_id]

    if tag_ids:
        payload["tags"] = tag_ids

    result = wp_request(
        "POST",
        "posts",
        json=payload,
    )

    return result, {
        "category_id": category_id,
        "tag_ids": tag_ids,
        "created_tags": created_tags,
        "excerpt_sent": bool(article.get("description")),
    }


def print_article_summary(article):
    """投稿前の確認情報を表示する"""
    print("\n=== 抽出結果 ===")
    print(f"タイトル: {article['title']}")
    print(f"スラッグ: {article['slug']}")

    if article.get("description"):
        print(f"メタディスクリプション / 抜粋: {article['description']}")
    else:
        print("メタディスクリプション / 抜粋: なし")

    if article.get("category_name"):
        print(f"カテゴリ: {article['category_name']}")
    else:
        print("カテゴリ: なし")

    if article.get("tag_names"):
        print("タグ:")
        for tag in article["tag_names"]:
            print(f"- {tag}")
    else:
        print("タグ: なし")

    print(f"本文HTML文字数: {len(article['raw_html'])}")
    print("本文抽出方式: WP_BODY_HTML_START / WP_BODY_HTML_END マーカー")
    print("投稿形式: カスタムHTMLブロック")
    print("投稿ステータス: 下書き")


def main():
    print("WordPress下書き投稿ツール")
    print("※ .env の中身、アプリケーションパスワードは絶対に共有しないでください。")
    print(f"※ 認証情報の読み込み先: {ENV_PATH}")
    print("※ wp-auto-post はローカル専用運用を推奨します。GitHubに上げる必要はありません。")

    file_path = input("\n投稿する _wp.md ファイルのパスを入力してください: ").strip()

    try:
        article = build_article_from_wp_md(file_path)
        raw_html_path, wp_content_path = save_debug_files(article)

        print_article_summary(article)

        print("\n確認用ファイルを作成しました。")
        print(f"- 抽出した本文HTML: {raw_html_path}")
        print(f"- WordPress投稿用HTML: {wp_content_path}")

        print("\nカテゴリ・タグ・抜粋の扱い:")
        print("- カテゴリ: WordPress上に同名カテゴリがある場合のみ設定。なければ未設定。自動作成しません。")
        print("- タグ: WordPress上になければ自動作成します。")
        print("- メタディスクリプション: WordPress標準の「抜粋」欄に送信します。")

        print("\nこの内容をWordPressに下書き投稿します。")
        confirm = input("続行しますか? yes / no: ").strip().lower()

        if confirm != "yes":
            print("投稿をキャンセルしました。")
            return

        result, post_result = create_draft_post(article)

        post_id = result.get("id")
        edit_url = f"{WP_SITE_URL.rstrip('/')}/wp-admin/post.php?post={post_id}&action=edit"

        print("\nWordPressに下書き投稿しました。")
        print(f"タイトル: {article['title']}")
        print(f"投稿ID: {post_id}")
        print(f"編集URL: {edit_url}")

        if post_result.get("category_id"):
            print(f"カテゴリID: {post_result['category_id']} を設定しました。")
        else:
            print("カテゴリ: 未設定です。カテゴリ名が空、またはWordPress上に同名カテゴリが見つかりませんでした。")

        if post_result.get("tag_ids"):
            print(f"タグID: {post_result['tag_ids']} を設定しました。")
        else:
            print("タグ: 未設定です。")

        if post_result.get("created_tags"):
            print("新規作成したタグ:")
            for tag in post_result["created_tags"]:
                print(f"- {tag}")

        if post_result.get("excerpt_sent"):
            print("メタディスクリプション: WordPress標準の「抜粋」欄へ送信しました。")
        else:
            print("メタディスクリプション: 未送信です。")

        print("\n次にWordPress側で確認してください。")
        print("- 下書きになっているか")
        print("- カスタムHTMLブロックになっているか")
        print("- プレビュー表示が崩れていないか")
        print("- 画像が表示されているか")
        print("- リンクが正しいか")
        print("- カテゴリが意図通りか")
        print("- タグが意図通りか")
        print("- 右サイドバー等の「抜粋」欄にメタディスクリプションが入っているか")

        print("\n※本文HTMLは加工せず、カスタムHTMLブロックとして投稿しています。")

    except Exception as e:
        print("\nエラーが発生しました。")
        print(str(e))
        sys.exit(1)


if __name__ == "__main__":
    main()

AGENTS.md

# AGENTS.md

## プロジェクト概要

このプロジェクトは、WordPress へ下書き投稿するためのローカル専用ツールです。

`post_to_wp.py` を使って、指定フォルダ内の `*_wp.md` ファイルを WordPress へ下書き投稿します。

- このプロジェクトを GitHub に上げる必要はありません。
- 投稿ステータスは必ず `draft` にします。
- 本文はカスタム HTML ブロックとして投稿します。
- SWELL の HTML 構造を壊さないように扱います。

## 秘密情報の扱い

秘密情報は絶対に表示・出力しません。

禁止事項:

- `~/.wp-auto-post.env` の中身を表示しない。
- `cat ~/.wp-auto-post.env` を実行しない。
- `printenv` を実行しない。
- `.env` を作成しない。
- `cat .env` を実行しない。
- WordPress のアプリケーションパスワードなど、秘密情報を出力しない。

## 投稿実行ルール

ユーザーが「下書き投稿して」「WP投稿して」「このフォルダを投稿して」などと依頼した場合は、指定フォルダ内から `*_wp.md` を探します。

対象フォルダが明示されていない場合は、勝手に広範囲を検索せず、ユーザーに対象フォルダの指定を求めます。

- `*_wp.md` が 0 件の場合:
  - 投稿は実行せず、「対象ファイルなし」と報告します。

- `*_wp.md` が 2 件以上ある場合:
  - 投稿は実行せず、候補ファイル一覧を表示してユーザーに確認します。

- `*_wp.md` が 1 件だけの場合:
  - そのファイルを投稿対象にします。

投稿実行前に、対象ファイルから抽出される以下の情報だけを確認表示します。

- タイトル
- スラッグ
- カテゴリ
- タグ
- 抜粋

ユーザー確認後、問題なければ `post_to_wp.py` を使って WordPress へ下書き投稿します。

投稿完了後は、WordPress の編集 URL をユーザーに伝えます。

## 許可してよい操作

以下の操作は、このプロジェクトの目的に沿う範囲で許可します。

- `python3 post_to_wp.py` の実行
- WordPress REST API へのネットワークアクセス
- `~/wp-auto-post-debug/` への debug ファイル書き込み

## 止めるべき操作

以下の操作は行いません。

- `cat ~/.wp-auto-post.env`
- `printenv`
- `cat .env`
- `.env` の作成
- 投稿ステータスを `publish` にする処理

## コード変更時のルール

コードを変更する場合は、以下を守ります。

- 勝手に大きなリファクタリングをしません。
- セキュリティ関連の挙動を変更する場合は、事前にユーザーへ確認します。
- WordPress 投稿処理を変更する場合は、必ず `draft` 固定を維持します。
- `response.text` を長文で出力しません。
- `WP_SITE_URL` の HTTPS チェックを維持します。

wp-md-format.md

# WordPress投稿用 `_wp.md` 出力ルール

ブログ記事をWordPressへ下書き投稿するための `_wp.md` ファイルを作成する際のルール。
このファイルは自動投稿プログラム(wp-auto-post)が読み取るため、出力形式を絶対に崩さないこと。

## 目的

以下の情報を、決められた形式で出力する。

- タイトル
- スラッグ
- メタディスクリプション
- カテゴリ
- タグ
- 投稿前チェック
- 本文HTML

## ファイル名ルール

完成ファイル名は必ず以下の形式。

例:`claude-cowork-guide-2026_wp.md`

## 出力全体の構造

必ず以下の順番で出力する。

# WordPress投稿チェックシート|{記事タイトル}

このファイルはWordPress下書き投稿用のチェックシートです。
自動投稿プログラムが読み取るため、形式を変更しないでください。

---

## 1. メタ情報(投稿画面で入力)

| 項目 | 値 |
|---|---|
| **タイトル** | {記事タイトル} |
| **スラッグ** | {slug} |
| **メタディスクリプション** | {120字前後の説明文} |
| **カテゴリ** | {既存カテゴリ名。未定なら空欄} |
| **タグ** | {タグ1 / タグ2 / タグ3} |

---

## 2. 投稿前チェック

- [ ] アイキャッチ画像を設定
- [ ] スラッグを確認
- [ ] カテゴリを確認
- [ ] メタディスクリプションを確認
- [ ] タグを確認
- [ ] 内部リンクを確認
- [ ] 画像の表示を確認
- [ ] リンク切れがないか確認
- [ ] WordPressプレビューで表示崩れがないか確認
- [ ] TODOコメントや仮リンクが残っていないか確認

---

## 3. 本文HTML(自動投稿プログラム用)

<!-- WP_BODY_HTML_START -->
{本文HTML}
<!-- WP_BODY_HTML_END -->

## 厳守ルール

### メタ情報テーブル

必ずこの形式で出力する。

- 項目名の変更禁止
- 項目順の変更禁止
- `**タイトル**` などの太字記法を削除しない
- 表形式以外で出力しない
- タグを箇条書きにしない

### スラッグ

- 英数字とハイフンのみ
- 小文字のみ
- 日本語禁止
- アンダースコア禁止
- スペース禁止
- 数字始まり禁止
- 末尾をハイフンにしない
- 連続ハイフンを使わない

良い例:`claude-cowork-guide-2026`

### タグ表記

タグは必ず `/` 区切り。

良い例:`Claude Cowork / Claude / AIエージェント`

### カテゴリ

既存カテゴリ名を1つだけ入れる。
不明な場合は空欄。
新しいカテゴリ名を勝手に作らない。

### 投稿前チェック

固定項目を必ず全て入れる。削除・変更禁止。
記事固有のチェック項目は固定項目の下に追加してOK。

### 本文HTML

必ず以下のマーカーで囲む。

<!-- WP_BODY_HTML_START -->
本文HTML
<!-- WP_BODY_HTML_END -->

禁止事項:

- ```html のコードブロックで囲むこと
- マーカー外に本文HTMLを書くこと
- <!-- wp:html --> や <!-- /wp:html --> を入れること
- Markdownを混ぜること

### 本文中のコードサンプル

技術記事でコードを載せる場合は、HTMLとして記述する。

良い例:
<pre><code>print("hello")</code></pre>

Markdownのコードフェンスを本文HTML内に入れない。

HTML特殊文字はエスケープする:
<pre><code>&lt;div&gt;Hello&lt;/div&gt;</code></pre>

## 本文HTMLの方針

- SWELLの邪魔をしない
- HTML構造を勝手に変更しない
- WordPress通常ブロック化を前提にしない
- カスタムHTMLブロックに貼り付ける前提で出力する

## 投稿ステータス

このファイルは下書き投稿用。公開処理は人間がWordPress側で確認後に行う。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

AIをガチで使い倒しているウルフ系ブロガー、のたです。
毎日Claude・ChatGPT・Geminiを使いながら、「AIで何ができるか」を実験中。
このブログでは、実際に使って感じたことをそのまま書いています。

目次