目次
- この記事で作る「WordPress自動投稿」の全体像
- 仕組みの概要
- 手動作業との比較
- 対応環境
- 事前準備 ── アプリケーションパスワードと .env
- アプリケーションパスワードの発行手順
- .env ファイルの設計
- frontmatter 設計 ── parse_post.py が読む7項目
- 必須7項目の設計と理由
- parse_front_matter の処理ロジック
- 制作メモ混入検知(check_production_meta_contamination)
- 投稿処理の7ステップ ── publish_post.py の全フロー
- Step 1-2 ── .env 読込 + Markdown 解析
- Step 3 ── Markdown → HTML 変換
- Step 4 ── カテゴリ・タグの自動解決
- Step 5 ── アイキャッチ画像のアップロード
- Step 6 ── SEO プラグイン連携(3プラグイン対応)
- Step 7 ── WordPress REST API への POST
- 品質を守る仕組み ── 公開前チェックと公開後検査
- 品質審査の合格確認(check_review_approved)
- 公開後の自動検査(verify_published_post)
- 制作メモ混入の自動ブロック(運用フローとして)
- 実行コマンドと運用フロー
- 新規公開の実行
- 既存記事の更新
- Claude Code に投稿スクリプトを作らせるときの指示例
- つまずき6選 ── 実際に起きたエラーと解決
- つまずき1:frontmatter が本文に混入した
- つまずき2:categories でエラー
- つまずき3:.env の設定が読まれない
- つまずき4:SEO メタが反映されない
- つまずき5:アイキャッチが記事に紐づかない
- つまずき6:カテゴリが重複生成された
- よくある質問
- Q1: プログラミング未経験でもこのスクリプトは使えるか
- Q2: 自分のブログの SEO プラグインに対応しているか
- まとめ
Claude CodeでWordPress投稿を自動化する方法|publish_post.pyの実装と運用記録
WordPress への投稿作業は、frontmatter 設計と Python スクリプト1本で「コマンド1発」にできます。しかも品質チェックや公開後検査まで自動化すれば、事故なく運用を続けられます。
この記事は、AIでブログ運営を自動化する5ステップの Step 3「WordPress に自動投稿する」 を完全に深掘りしたものです。ピラー記事では500字ほどの概要でしたが、ここでは実際に使っている publish_post.py のコードを断片的に示しながら、7ステップの処理フロー・frontmatter の設計思想・つまずき6件の解決策まで、手元で再現できるレベルで解説します。
私自身は看護師で、プログラミングは未経験のまま Claude Code にスクリプトを作らせてここまで来ました。月25本・延べ30スクリプト以上を動かしてきた中で見えてきたことを、そのまま書いておきます。
この記事で作る「WordPress自動投稿」の全体像
最初に処理の流れを整理しておきます。
仕組みの概要
WordPress は REST API を持っています。HTTP リクエストを送れば、外部のスクリプトから記事を投稿・更新できます。publish_post.py はその仕組みを使って、以下の7ステップを順番に実行します。
Step 1: .env を読み込んで認証情報を取得
Step 2: Markdown ファイルを読んで frontmatter と本文を分離
Step 3: 本文を Markdown → HTML に変換
Step 4: カテゴリ・タグの名前を WordPress 上の ID に変換
Step 5: アイキャッチ画像をアップロード
Step 6: SEO プラグイン向けのメタ情報を組み立てる
Step 7: WordPress REST API に投稿データを POST
実行時間は画像のサイズによりますが、通常 5〜15秒 で完了します。
手動作業との比較
| 作業 | 手動 | 自動(スクリプト) |
|---|---|---|
| 本文をコピペ | 毎回 | 不要(Markdown から直接投稿) |
| カテゴリ・タグの選択 | 毎回クリック | frontmatter に書くだけ |
| アイキャッチのアップロード | 毎回 | スクリプトが自動アップロード |
| SEO タイトル・説明文の入力 | 毎回コピペ | frontmatter から自動反映 |
| 公開後の設定確認 | 目視 | 7項目を自動検査 |
手動で30分かかっていた作業が、コマンド1行で済むようになります。
対応環境
- Python 3.10 以上
- WordPress 5.6 以上(REST API の標準実装)
- HTTPS 必須(アプリケーションパスワード認証の要件)
- 外部ライブラリ:
requests/python-dotenv/markdown(いずれもpip installで入る)
事前準備 ── アプリケーションパスワードと .env
スクリプトを動かす前に、2つの設定が必要です。アプリケーションパスワードの発行と、認証情報を書く .env ファイルの作成です。
アプリケーションパスワードの発行手順
WordPress には「アプリケーションパスワード」という機能があります。通常のログインパスワードとは別に、外部からの API アクセス専用のパスワードを発行できます。これを使うことで、本体のパスワードを外部に渡さずに済みます。
- WordPress 管理画面にログインする
- 左メニュー「ユーザー」→「プロフィール」を開く
- ページを下にスクロールし、「アプリケーションパスワード」の項目を探す
- 「新しいアプリケーションパスワードの名前」に任意の名前を入力する(例:
claude-auto-post) - 「新しいアプリケーションパスワードを追加」ボタンをクリックする
- 表示されたパスワード(
xxxx xxxx xxxx xxxx xxxx xxxx形式)をすぐにコピーする
この画面を閉じると二度と確認できません。 必ずコピーしてから先へ進んでください。もしコピーし忘れた場合は、そのパスワードを削除してもう一度作り直す必要があります。
SiteGuard 等のセキュリティプラグインとの衝突
SiteGuard WP Plugin などのセキュリティプラグインを使っている場合、REST API へのアクセスがブロックされることがあります。その場合は一時的にプラグインを無効化するか、REST API のホワイトリストに開発元 IP を追加する対応が必要です。「認証は合っているのに 403 になる」という場合は、このパターンが多いです。
Claude Code が未導入の場合は先に設定が必要です。始め方は別記事「Claude Code の始め方ガイド」を参照してください。
.env ファイルの設計
認証情報は .env ファイルに書いて管理します。スクリプトを実行するフォルダのルートに置いてください。
WP_BASE_URL=https://your-site.com
WP_USERNAME=your-wp-username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
WP_SEO_PLUGIN=seopress
キー名は上記の名前をそのまま使ってください。 スクリプト内の load_settings() 関数がこれらの名前で値を読み込む設計になっています。BLOG_URL=... や WP_PASSWORD=... のように独自の名前に変えると、スクリプトが設定を見つけられずエラーになります(つまずき3番、後述)。
WP_SEO_PLUGIN には使っている SEO プラグインを指定します。選択肢は seopress / yoast / rankmath / none の4つです。
def load_settings():
load_dotenv(dotenv_path=Path(".env"))
base_url = os.getenv("WP_BASE_URL")
username = os.getenv("WP_USERNAME")
app_password = os.getenv("WP_APP_PASSWORD")
seo_plugin = os.getenv("WP_SEO_PLUGIN", "yoast").lower()
# 不足があれば ValueError を raise する
...
.gitignore に追加する(必須)
.env には認証情報が入っているため、Git にコミットしてはいけません。プロジェクトフォルダの .gitignore に以下を追加してください。
.env
これを忘れると、リモートリポジトリに認証情報が上がってしまいます。実際に「.env が GitHub にコミットされかけた」というヒヤリハットは起きています(私が経験したつまずきです)。
frontmatter 設計 ── parse_post.py が読む7項目
投稿する Markdown ファイルの冒頭には、frontmatter を書きます。--- で囲まれた YAML ブロックで、記事のメタ情報を定義する部分です。
必須7項目の設計と理由
parse_post.py の REQUIRED_FIELDS には7項目が定義されています。
REQUIRED_FIELDS = [
"title",
"slug",
"categories",
"tags",
"excerpt",
"eyecatch",
"eyecatch_alt",
]
これらが揃っていない状態で publish_post.py を実行すると、投稿の前に validate_fields() がチェックして不足項目を表示し、処理を止めます。
実際の frontmatter のサンプルはこのような形です。
---
title: "看護師が夜勤で感じる精神的消耗について"
slug: nurse-night-shift-mental
seo_title: "夜勤がきつい看護師へ|精神的消耗を減らす考え方3つ"
seo_description: "夜勤のきつさは体力より精神的な消耗が深刻。現役看護師が実際に感じた場面をもとに解説します。"
excerpt: "夜勤のきつさは体力より精神的な消耗が深刻。現役看護師が実際に感じた場面をもとに解説します。"
categories:
- 看護師の働き方
tags:
- 夜勤
- 精神的消耗
- 看護師
status: publish
eyecatch: images/nurse-night-shift-mental_eyecatch.png
eyecatch_alt: "夜勤明けに空を見上げる看護師"
---
categories と tags は必ず YAML のリスト形式で書いてください。
# 正しい
categories:
- 看護師の働き方
# これはエラーになる
categories: 看護師の働き方
category: ...(単数・文字列形式)で書くと、スクリプトがリストとして処理できずエラーになります。これが一番多いつまずきです(つまずき2番、後述)。
parse_front_matter の処理ロジック
parse_post.py の parse_front_matter() 関数は、以下の流れで frontmatter を辞書に変換します。
def parse_front_matter(text: str):
# 1. --- で囲まれた部分を正規表現で抜き出す
pattern = r"^---\n(.*?)\n---\n(.*)$"
match = re.match(pattern, text, re.DOTALL)
# 2. 行ごとにパース
LIST_FIELDS = {"tags", "categories"}
for line in front_matter_text.splitlines():
if re.match(r"^\s*-\s+", line) and current_key in LIST_FIELDS:
# リスト項目
item = _strip_yaml_quotes(re.sub(r"^\s*-\s+", "", line).strip())
data.setdefault(current_key, []).append(item)
elif ":" in line:
# キー: 値
key, value = line.split(":", 1)
data[key.strip()] = _strip_yaml_quotes(value.strip())
_strip_yaml_quotes() は値を囲む "..." や '...' を取り除く関数です。frontmatter 内で categories: "看護師の働き方" のようにクオートが残っていると、WordPress に "看護師の働き方" という名前のカテゴリが作られてしまいます(つまずき6番、後述)。この関数がそれを防いでいます。
def _strip_yaml_quotes(value: str) -> str:
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
return value[1:-1]
return value
制作メモ混入検知(check_production_meta_contamination)
2026-04-21 に本番環境で実際に起きた事故があります。記事の本文末に「自己チェック」「メタ情報案」などの制作メモが残ったまま公開されてしまいました。
この再発を防ぐために、parse_post.py に16パターンの検知ロジックを追加しています。
PRODUCTION_META_PATTERNS = [
r"自己チェック",
r"品質審査前確認",
r"品質審査",
r"メタ情報案",
r"タイトル案",
r"SEOタイトル案",
r"meta\s*description\s*案",
r"v\d+修正", # v2修正, v3修正...
r"修正メモ",
r"制作メモ",
r"編集メモ",
r"構成メモ",
r"タグ候補",
r"カテゴリ候補",
r"スラッグ案",
r"slug案",
]
本文にこれらのパターンが含まれていると、check_production_meta_contamination() が検出して公開を自動ブロックします。
❌ 本文に制作メモが混入しています:
- [自己チェック] L120: ## 自己チェック
公開を中止しました。制作メモを本文から削除してください。
どうしても公開が必要な場合のみ、--allow-meta-keywords オプションで回避できます。通常は使わないフラグです。
投稿処理の7ステップ ── publish_post.py の全フロー
実際の処理を1ステップずつ見ていきます。
Step 1-2 ── .env 読込 + Markdown 解析
# Step 1
settings = load_settings()
# → WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD / WP_SEO_PLUGIN を読み込む
# Step 2
frontmatter, body = parse_post_file(md_path)
# → parse_front_matter() で frontmatter を辞書に変換
# → validate_fields() で必須7項目の存在を確認
# → check_production_meta_contamination() で制作メモの混入を検出
フォルダを引数に渡した場合、resolve_input_path() が 04_draft_v{N}.md の最新バージョンを自動検出します。バージョン番号が最も大きいファイルが選ばれます。
Step 3 ── Markdown → HTML 変換
def simple_markdown_to_html(text: str) -> str:
extensions = [
"tables", # | テーブル | 対応 |
"nl2br", # 改行を <br> に変換
"fenced_code", # コードブロック対応(```python ... ```)
]
return md_lib.markdown(text, extensions=extensions)
WordPress の Gutenberg(ブロックエディタ)は HTML を直接扱えるため、Markdown のまま送らずに HTML に変換してから投稿します。Markdown のまま送ると、見出しが # そのまま表示されたり、テーブルが崩れたりします。
Step 4 ── カテゴリ・タグの自動解決
frontmatter には categories: - 看護師の働き方 のように名前で書きますが、WordPress REST API に渡す際は数値 ID が必要です。resolve_term_ids() がこの変換を担います。
def fetch_all_terms(settings: dict, taxonomy: str) -> list[dict]:
# REST API で全カテゴリ(全タグ)を一括取得
# WordPress の search パラメータは日本語をトークナイズするため
# 全件取得 → ローカル完全一致の方が確実
url = f"{settings['base_url']}/wp-json/wp/v2/{taxonomy}"
# ページネーションで全件取得(per_page=100 で最大50ページ)
...
def resolve_term_ids(settings, taxonomy, names, allow_create=True):
term_ids = []
for raw_name in names:
name = _normalize_term_key(raw_name)
term = find_term_by_name(settings, taxonomy, name)
if term is None:
if not allow_create:
raise RuntimeError(f"'{name}' が WordPress に存在しません")
term = create_term(settings, taxonomy, name)
term_ids.append(term["id"])
return term_ids
カテゴリには --allow-new-categories という安全装置が付いています。このオプションなしでは、WordPress に存在しないカテゴリ名が frontmatter にあるとエラーで止まります。タイポによって 看護師の働き方2 のような不要なカテゴリが量産されるのを防ぐための設計です。
タグは記事ごとに新規が増えやすいため、デフォルトで自動作成します。
Step 5 ── アイキャッチ画像のアップロード
def upload_media(settings: dict, image_path: Path, alt_text: str):
media_url = f"{settings['base_url']}/wp-json/wp/v2/media"
# MIME タイプを自動推定
mime_type, _ = mimetypes.guess_type(str(image_path))
with open(image_path, "rb") as f:
headers = {
"Content-Disposition": f'attachment; filename="{image_path.name}"',
"Content-Type": mime_type,
}
response = requests.post(
media_url,
auth=get_auth(settings),
headers=headers,
data=f.read(),
)
media_id = response.json().get("id")
# alt テキストを別途 PATCH で設定
requests.post(
f"{settings['base_url']}/wp-json/wp/v2/media/{media_id}",
auth=get_auth(settings),
json={"alt_text": alt_text},
)
return media_id
アップロードして返ってきた media_id を、後の build_post_payload() で featured_media フィールドに設定します。「アップロードする」と「記事に紐づける」は別のリクエストになっています。
eyecatch が未設定の場合はアップロードをスキップします。更新時に media_id = 0 を送ると既存のアイキャッチが削除されるため、0 の場合は featured_media フィールド自体を送らない設計になっています(つまずき5番、後述)。
Step 6 ── SEO プラグイン連携(3プラグイン対応)
SEO プラグインのメタフィールド名は、プラグインごとに異なります。build_seo_meta() がその変換を担います。
def build_seo_meta(frontmatter: dict, seo_plugin: str) -> dict:
seo_title = frontmatter.get("seo_title", "")
seo_desc = frontmatter.get("seo_description", "")
if seo_plugin == "seopress":
return {
"_seopress_titles_title": seo_title,
"_seopress_titles_desc": seo_desc,
}
elif seo_plugin == "yoast":
return {
"_yoast_wpseo_title": seo_title,
"_yoast_wpseo_metadesc": seo_desc,
}
elif seo_plugin == "rankmath":
return {
"rank_math_title": seo_title,
"rank_math_description": seo_desc,
}
else:
return {}
これらのメタフィールドは「保護フィールド(protected field)」として扱われるため、デフォルトでは REST API 経由で書き込めません。register_post_meta を PHP コードで WordPress に登録することで、書き込みが可能になります(つまずき4番、後述)。
SEO メタの自動生成については、別記事「AI で SEO タイトル・メタディスクリプションを自動生成する方法(準備中)」で詳しく解説しています。
Step 7 ── WordPress REST API への POST
def build_post_payload(frontmatter, body_html, media_id,
category_ids, tag_ids, seo_meta, include_status):
payload = {
"title": frontmatter["title"],
"slug": frontmatter["slug"],
"excerpt": frontmatter["excerpt"],
"content": body_html,
"categories": category_ids,
"tags": tag_ids,
}
# 新規作成時のみ status を指定する
# 更新時は既存のステータスを維持するため status を送らない
if include_status:
fm_status = frontmatter.get("status", "publish")
payload["status"] = fm_status or "publish"
# アイキャッチがある場合のみ featured_media を送る
if media_id and media_id > 0:
payload["featured_media"] = media_id
if seo_meta:
payload["meta"] = seo_meta
return payload
新規公開と更新で処理を分けています。
- 新規公開:
POST /wp-json/wp/v2/posts(statusを含む) - 更新:
POST /wp-json/wp/v2/posts/{id}(statusを含まない)
更新時に status を送ってしまうと、下書きの記事が publish になってしまうリスクがあります。include_status=False にすることで既存ステータスを維持します。
バックデート投稿にも対応しています。frontmatter に date: "2026-02-07T10:00:00" のように書くと、その日時での投稿になります。
品質を守る仕組み ── 公開前チェックと公開後検査
これは上位10記事のどれにも書かれていない機能です。「投稿できた」で終わらず、品質と正確性を担保する仕組みを組み込んでいます。
品質審査の合格確認(check_review_approved)
フォルダモードで新規公開する場合、スクリプトは自動的に 05_review_v{N}.md の存在と合格判定を確認します。
def check_review_approved(folder: Path, version: int) -> bool:
review_path = folder / f"05_review_v{version}.md"
if not review_path.exists():
return False
text = review_path.read_text(encoding="utf-8")
# 「## 判定」または「### 判定」の直後に「合格」があれば合格扱い
m = re.search(r"#{2,3}\s*判定\s*\n+\s*([^\n]+)", text)
if not m:
return False
return "合格" in m.group(1) and "差し戻し" not in m.group(1)
05_review_v{N}.md が存在しない、または「差し戻し」と書かれている場合は、公開を自動停止します。
❌ 05_review_v1.md が存在しないか、判定が「合格」ではありません。
審査を通してから公開してください。
(緊急時は --skip-review-check で回避可能)
緊急時のみ --skip-review-check で回避できます。
公開後の自動検査(verify_published_post)
公開が完了すると、7項目の自動検査が走ります。
checks = [
("公開ステータス", verify.get("status")), # publish になっているか
("カテゴリ設定", verify.get("categories")), # 1個以上設定されているか
("タグ設定", verify.get("tags")),
("アイキャッチ", verify.get("featured_image")),
("SEOタイトル", verify.get("seo_title")), # SEOプラグインに反映されているか
("SEO説明文", verify.get("seo_description")),
("ライブURL", verify.get("live_url")), # HTTP 200 で応答するか
]
実行結果はこのような形式で出力されます。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
自動検査結果
✅ 公開ステータス
✅ カテゴリ設定
✅ タグ設定
❌ アイキャッチ
❌ SEOタイトル
❌ SEO説明文
✅ ライブURL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ ❌ の項目は WordPress 管理画面で確認してください
SEO が未反映の場合は、手動入力ガイドが自動表示されます。プラグイン名・編集画面の URL・コピー用テキストがセットで出てくるので、そのまま管理画面で貼り付けられます。
制作メモ混入の自動ブロック(運用フローとして)
公開フロー全体での動きを整理すると以下のようになります。
① frontmatter を parse する
② 必須7項目の存在を確認する → 不足があれば停止
③ 本文に制作メモが含まれていないか確認する → 含まれていれば停止
④ 品質審査ファイルの合格判定を確認する → 未合格なら停止
⑤ すべてクリアしたら投稿処理へ
この4重のチェックを経て初めて WordPress への通信が走ります。「書いてコマンド1発」でも、品質が担保された状態で投稿されることを目指した設計です。
実行コマンドと運用フロー
新規公開の実行
python scripts/publish_post.py drafts/articles/my-article/
フォルダを渡すと、スクリプトは以下を自動で行います。
- フォルダ内で最新の
04_draft_v{N}.mdを検出する - 対応する
05_review_v{N}.mdの合格判定を確認する - 7ステップの投稿処理を実行する
- 公開後の7項目自動検査を実行する
done/{slug}_v{N}.mdに公開用ファイルをコピーするdrafts/articles/{slug}/をdone/history/{slug}/にアーカイブする
既存記事の更新
python scripts/publish_post.py drafts/articles/my-article/ --update 3205
--update に WordPress の投稿 ID を渡すと更新モードになります。更新時は以下の違いがあります。
statusフィールドを送らない(既存ステータスを維持)- 品質審査チェックをスキップする(更新は任意判断)
done/へのファイル移動を行わないmedia_id = 0の場合にfeatured_mediaを送らない(既存アイキャッチ保護)
投稿 ID は WordPress 管理画面の投稿一覧で確認できます。URL の ?post=3205 の数字が ID です。
Claude Code に投稿スクリプトを作らせるときの指示例
自分でゼロから作るのではなく、Claude Code に作ってもらう場合の指示例です。
最初の1回:スクリプト作成依頼
Markdown ファイルを WordPress に投稿する Python スクリプトを作ってください。
要件:
- 認証情報は .env ファイルから読み込む(キー名:WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD)
- Markdown 冒頭の frontmatter(---〜--- の YAML ブロック)から title / slug / categories / tags / excerpt を取得する
- categories と tags は YAML リスト形式で書かれている想定
- Markdown 本文を HTML に変換して投稿する
- WordPress REST API(/wp-json/wp/v2/posts)に POST する
追加仕様:アイキャッチ・SEO 対応
先ほどのスクリプトに以下を追加してください。
1. frontmatter の eyecatch パスから画像を読み込み、/wp-json/wp/v2/media にアップロードする
2. アップロードした media_id を featured_media に設定する
3. frontmatter の seo_title / seo_description を、SEOPress 向けのメタフィールドとして meta に含める
(キー名:_seopress_titles_title / _seopress_titles_desc)
「エラーが出たら貼り付けて聞く」でここまで来られます。自分でコードを書く必要はありません。
つまずき6選 ── 実際に起きたエラーと解決
月25本・30スクリプト以上の実運用の中で遭遇した6件のつまずきです。原因と解決を具体名で書いておきます。
つまずき1:frontmatter が本文に混入した
症状:WordPress に投稿すると、記事の先頭に --- と YAML のテキストがそのまま表示される。
原因:parse_front_matter() の正規表現が frontmatter を正しく切り出せなかったケースです。frontmatter 内に「内容チェック」という文字列を含むフィールドを作ったことがあり、これがスクリプトの誤検出を引き起こしました。
解決:frontmatter のフィールド名・値に特殊なキーワードを使わない。また check_production_meta_contamination() を追加して、本文に制作メモが混入していないかを事前に検出するようにしました。
つまずき2:categories でエラー
症状:ValueError: 必須項目が不足しています: categories が出る。frontmatter に categories は書いているのに。
原因:frontmatter を categories: 看護師の働き方(単数・文字列形式)で書いていた。スクリプトはリスト形式を期待しているため、空リストとして処理されていました。
解決:
# これが正しい
categories:
- 看護師の働き方
YAML リスト形式(- 項目名)に変更する。frontmatter を書くときのルールとして最初に覚えておくべき点です。
つまずき3:.env の設定が読まれない
症状:ValueError: .env に不足があります: WP_BASE_URL, WP_USERNAME と表示される。.env には書いたはずなのに。
原因:キー名を独自に変更していた(BLOG_URL=...、WP_PASSWORD=... 等)。スクリプトは WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD という名前で読み込む固定設計のため、名前が違うと見つからない。
解決:.env のキー名を規定の3つに統一する。
WP_BASE_URL=https://your-site.com
WP_USERNAME=your-wp-username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
つまずき4:SEO メタが反映されない
症状:スクリプトは正常終了するが、公開後の自動検査で「❌ SEOタイトル / ❌ SEO説明文」が表示される。管理画面で確認すると SEO 欄が空のまま。
原因:SEOPress・Yoast などのメタフィールドは「保護フィールド(protected field)」として設定されており、デフォルトでは WordPress REST API からの書き込みが拒否される。
解決:WordPress に register_post_meta を登録して、REST API からの書き込みを許可する。WPCode(旧 Code Snippets)プラグインで以下の PHP スニペットを追加します。
add_action('init', function() {
register_post_meta('post', '_seopress_titles_title', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
]);
register_post_meta('post', '_seopress_titles_desc', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
]);
});
Yoast の場合は _yoast_wpseo_title / _yoast_wpseo_metadesc、RankMath の場合は rank_math_title / rank_math_description に変えて同様に登録します。
REST API つまずき集の詳細解説は「WordPress REST API つまずき集」で公開予定です。
つまずき5:アイキャッチが記事に紐づかない
症状:スクリプトが正常終了し「アイキャッチID : 1234」と表示されるが、公開された記事にアイキャッチが表示されない。
原因:アップロードした画像を featured_media に設定する処理が抜けていた、または media_id = 0 のまま featured_media: 0 を送っていた。0 を送ると WordPress が「アイキャッチなし」として処理します。
解決:upload_media() がアップロードと alt 設定をまとめて返す設計にした。media_id > 0 の場合のみ featured_media フィールドを payload に含めるようにした。
if media_id and media_id > 0:
payload["featured_media"] = media_id
つまずき6:カテゴリが重複生成された
症状:WordPress の管理画面でカテゴリを確認すると、看護師の働き方 と "看護師の働き方" が両方存在している。
原因:frontmatter で categories: - "看護師の働き方"(値をクオートで囲む)と書いていた。スクリプトがクオートを取り除かずに WordPress に渡したため、"看護師の働き方" という名前のカテゴリが新規作成された。
解決:parse_post.py に _strip_yaml_quotes() を追加して、パース時にクオートを自動除去するようにした。
def _strip_yaml_quotes(value: str) -> str:
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
return value[1:-1]
return value
よくある質問
Q1: プログラミング未経験でもこのスクリプトは使えるか
WordPress REST API の操作はコマンド1行で実行できます。コードを書く必要はなく、.env と frontmatter を設定してコマンドを実行するだけです。
私自身、非エンジニアのまま publish_post.py を月25本のペースで使っています。コードは Claude Code が書いてくれたもので、自分で書いた行はほぼゼロです。私がやっていることは「エラーメッセージを Claude Code に貼り付けて『これどういう意味?』と聞く」の繰り返しです。プログラミングスキルより、エラーを怖がらずに聞く習慣の方が重要だと感じています。
初期設定(アプリケーションパスワード発行・.env 作成・register_post_meta のスニペット登録)さえ済めば、あとは python scripts/publish_post.py drafts/articles/my-article/ の1行だけで動きます。
Q2: 自分のブログの SEO プラグインに対応しているか
WordPress の主要 SEO プラグインにはそれぞれ固有のメタフィールド名があります。build_seo_meta() は SEOPress / Yoast / RankMath の3プラグインに対応しています。.env の WP_SEO_PLUGIN に使っているプラグイン名を書くだけで切り替わります。
私は ryman-nurse.com では SEOPress、halolab.jp では SEO Simple Pack を使っています。SEO Simple Pack は上記3つに含まれないため、現在は WP_SEO_PLUGIN=none に設定して SEO メタは手動で入力しています(自動検査で「❌ SEOタイトル」が出ますが、手動ガイドに従って入力する運用です)。
none を指定した場合、スクリプトは SEO フィールドを送らず、公開後に手動入力用のガイドが表示されます。自分のプラグインが3つに含まれない場合は none で運用しながら、プラグイン固有のフィールド名を調べて追加するのが現実的な流れです。
まとめ
publish_post.py でやっていることを整理します。
.envから認証情報を読み込む- Markdown の frontmatter と本文を分離する(parse_post.py)
- 本文を HTML に変換する
- カテゴリ・タグ名を WordPress の ID に変換する
- アイキャッチ画像をアップロードする
- SEO プラグイン向けのメタ情報を組み立てる
- WordPress REST API に投稿する
品質を守るための仕組みとして、制作メモ混入検知(16パターン)・品質審査ファイルの合格確認・公開後7項目の自動検査を組み込んでいます。月25本・30スクリプト以上の実運用から生まれたつまずき6件も、具体名で記録しました。
「投稿できた」だけではなく「運用で壊れない」設計にしたことで、今もこの仕組みを安心して使い続けられています。
全体像が気になる方は、AIでブログ運営を自動化する5ステップに戻って他のステップも読んでみてください。Step 3 の前後にある Step 2(本文をAIと書く)や Step 4(アイキャッチ生成)と組み合わせると、一連のフローとして動くようになります。
実際に動かしている publish_post.py の全コードと運用記録は、note の有料記事で公開しています(500円)。
