目次
- Markdownで書いた記事をWordPressに自動投稿するとは
- プラグイン方式との違い
- frontmatter の設計 ── 記事のメタ情報を YAML で管理する
- 必須項目と任意項目
- SEOプラグイン連携(seo_title / seo_description)
- Markdown を HTML に変換する仕組み
- なぜ Python markdown ライブラリか
- バリデーション ── 投稿前に壊れた記事を止める
- 制作メモ混入検出とは
- コマンド1発で投稿する手順
- 手順1: frontmatter と本文を書いた Markdown ファイルを用意する
- 手順2: parse_post.py で単体検証する(任意)
- 手順3: publish_post.py でフォルダを指定して投稿する
- フォルダ構造モード vs 単一ファイルモード
- つまずきやすいポイントと解決策
- 1. categories を文字列で書くとエラー
- 2. YAML クオートの罠:タグがクオート付きで登録される
- 3. eyecatch パスの相対・絶対問題
- 4. SEOメタが反映されない
- 5. テーブルが変換されない
- よくある質問
- Q: frontmatter のカテゴリやタグに日本語を使っても大丈夫ですか?
- Q: Gutenberg エディターで開くと Markdown が崩れませんか?
- Q: SEOプラグインを使っていない場合はどうなりますか?
- まとめ ── Markdown を「書いたら投稿」の仕組みにする
Markdown記事をWordPressに自動投稿する流れ|frontmatter設計からコマンド1発まで
「記事は書けた。でも投稿がいつも面倒くさい」
タイトルを入れて、カテゴリを選んで、タグを設定して、アイキャッチをアップロードして、SEOプラグインにメタ説明を入力して……WordPress の管理画面でやる作業は地味に多いです。1本あたり10〜15分かかることもある。
私はこの「投稿作業」を、Markdown ファイルさえ書いてしまえばコマンド1発で終わる仕組みに変えました。記事ごとにブラウザを開かなくていい。カテゴリの設定漏れも起きない。制作メモが本文に混ざったまま公開してしまう事故も防げます。
この記事では、その仕組みの核にある parse_post.py(frontmatter 解析・バリデーション担当)の設計を中心に解説します。
- frontmatter の設計:WordPress のどの設定に何を書けばいいか
- Markdown→HTML 変換:どのライブラリを使い、なぜその設定なのか
- バリデーション:投稿前に壊れた記事を止める仕組み
- コマンド1発で投稿する手順:実行コマンドと出力の実例
REST API の認証・画像アップロード・カテゴリ解決の詳細は publish_post.py を解説した別記事で扱っています。全体像を先に把握したい場合はブログ自動化5ステップの全体像はこちらからどうぞ。
Markdownで書いた記事をWordPressに自動投稿するとは
「Markdown で WordPress に投稿する」という言葉は、2種類の意味で使われます。
ひとつは、WordPress の中で Markdown を使う方法です。Gutenberg エディターや Jetpack プラグインを使って、WordPress の管理画面上で Markdown 記法を使えるようにするアプローチです。検索すると、この方法の記事が大半を占めます。
もうひとつは、この記事で扱う方法、ローカルの Markdown ファイルを WordPress に自動投稿する方法です。手元のエディター(VS Code など)で .md ファイルを書き、Python スクリプトを実行すると WordPress 側の記事が作られる仕組みです。
プラグイン方式との違い
どちらが「正解」かではなく、目指すところが違います。
| 観点 | プラグイン方式 | スクリプト方式(本記事) |
|---|---|---|
| WordPress の管理画面 | 必要(記事ごとに開く) | 不要(コマンドで完結) |
| カテゴリ・タグ設定 | 管理画面で手動 | frontmatter に記述 |
| SEOプラグイン設定 | 管理画面で手動 | frontmatter に記述、自動反映 |
| アイキャッチ設定 | 管理画面でアップロード | ファイルパスを書くだけ |
| 複数記事の一括投稿 | できない | スクリプトで連鎖可 |
| バージョン管理(Git) | しにくい | Markdown なので自然にできる |
| 初期設定のコスト | 低い | やや高い(Python必要) |
スクリプト方式の最大のメリットは、記事に関するすべての設定を Markdown ファイル内に書けることです。「どの記事にどのカテゴリを設定したか」をコードベースで管理できます。
全体のデータの流れはシンプルです。
Markdown ファイル(frontmatter + 本文)
↓ parse_post.py が解析・バリデーション
frontmatter 辞書 + HTML 本文
↓ publish_post.py が REST API で送信
WordPress(下書き or 公開済み)
frontmatter の設計 ── 記事のメタ情報を YAML で管理する
frontmatter とは、Markdown ファイルの冒頭に --- で囲んで書く YAML 形式のメタ情報です。記事のタイトル・カテゴリ・タグ・アイキャッチなど、WordPress の管理画面で設定していた情報を、ここに全部書きます。
実際に運用しているフルサンプルを見るのが一番わかりやすいです。
---
title: "AIでブログ運営を自動化する5ステップ【非エンジニアの実例付き】"
slug: ai-blog-automation-howto
status: publish
date: 2026-05-06
categories:
- ブログ自動化
tags:
- AI
- Claude Code
- ブログ自動化
- WordPress
- 自動投稿
- 非エンジニア
seo_title: "AIブログ自動化の5ステップ|非エンジニアの実例付き"
seo_description: "AIでブログ運営を自動化する方法を5ステップで解説。..."
excerpt: "AIでブログ運営を自動化する方法を5ステップで解説。..."
eyecatch: images/ai-blog-automation-howto_eyecatch.png
eyecatch_alt: "暗い作業部屋でモニターに向かう非エンジニアのハロ"
---
各フィールドが WordPress のどの設定に対応するかを確認しておきます。
| frontmatter フィールド | WordPress 上の反映先 | 必須/任意 |
|---|---|---|
title |
記事タイトル | 必須 |
slug |
パーマリンク(URL) | 必須 |
categories |
カテゴリ(YAML リスト形式) | 必須 |
tags |
タグ(YAML リスト形式) | 必須 |
excerpt |
抜粋 | 必須 |
eyecatch |
アイキャッチ画像(ファイルパス) | 必須 |
eyecatch_alt |
アイキャッチの alt テキスト | 必須 |
status |
公開ステータス(publish / draft) | 任意(デフォルト: publish) |
date |
投稿日時(バックデート用) | 任意 |
seo_title |
SEOプラグインのタイトル | 任意だが推奨 |
seo_description |
SEOプラグインのメタ説明 | 任意だが推奨 |
必須項目と任意項目
parse_post.py には REQUIRED_FIELDS という定数があり、この7項目がそろっていないと投稿を止めます。
REQUIRED_FIELDS = [
"title",
"slug",
"categories",
"tags",
"excerpt",
"eyecatch",
"eyecatch_alt",
]
この7項目を必須にしたのは運用上の理由からです。タイトルと slug がなければ記事が成立しない。categories と tags がないと WordPress 側のデフォルトカテゴリに入ってしまう。excerpt は一覧ページの表示に影響する。eyecatch と eyecatch_alt はアイキャッチの表示とアクセシビリティに直結します。
どれか1つでも欠けると validate_fields() が不足項目のリストを返し、スクリプトが止まります。
def validate_fields(data: dict):
missing = [
field for field in REQUIRED_FIELDS
if field not in data or data[field] in ("", [])
]
return missing
欠けていた場合の出力はこうなります。
不足項目があります:
- excerpt
- eyecatch
「眠いまま書いて eyecatch を書き忘れた」というケースは実際に何度かありました。バリデーションがなければそのまま公開されていたと思います。
status と date は任意ですが、書いておくことを推奨します。status を書かない場合のデフォルトは publish(即公開)です。下書きとして保存したい場合は status: draft を明記します。
SEOプラグイン連携(seo_title / seo_description)
seo_title と seo_description は、SEOプラグインに自動反映されます。対応プラグインは SEOPress・Yoast SEO・RankMath の3つで、.env ファイルの WP_SEO_PLUGIN に設定した値によって転送先のフィールドが変わります。
build_seo_meta() の対応表です。
| プラグイン | SEOタイトルのフィールド名 | メタ説明のフィールド名 |
|---|---|---|
| SEOPress | _seopress_titles_title |
_seopress_titles_desc |
| Yoast SEO | _yoast_wpseo_title |
_yoast_wpseo_metadesc |
| RankMath | rank_math_title |
rank_math_description |
ただし、この自動反映には条件があります。WordPress 側で PHP の register_post_meta() を使って対象フィールドを REST API に公開する許可設定が必要です。これが設定されていないと、seo_title を frontmatter に書いても反映されません(後述の「つまずきやすいポイント」で詳しく説明します)。
SEOタイトル・メタディスクリプションの作り方自体を知りたい場合は、SEOタイトル・メタディスクリプションの作り方はこちらを参照してください。
Markdown を HTML に変換する仕組み
WordPress の REST API が受け取るのは HTML です。Markdown をそのまま送っても機能しません。publish_post.py 内の simple_markdown_to_html() が変換を担います。
import markdown as md_lib
def simple_markdown_to_html(text: str) -> str:
extensions = [
"tables", # | テーブル | 対応 |
"nl2br", # 改行を <br> に変換
"fenced_code", # コードブロック対応
]
return md_lib.markdown(text, extensions=extensions)
Before / After で変換の実例を見ておきます。
Markdown(入力):
## 見出し2
| 項目 | 内容 |
|------|------|
| Python | 3.10+ |
| WordPress | 5.6+ |
```python
python scripts/publish_post.py drafts/articles/my-post/
**HTML(出力):**
```html
<h2>見出し2</h2>
<table>
<thead><tr><th>項目</th><th>内容</th></tr></thead>
<tbody>
<tr><td>Python</td><td>3.10+</td></tr>
<tr><td>WordPress</td><td>5.6+</td></tr>
</tbody>
</table>
<pre><code class="language-python">python scripts/publish_post.py drafts/articles/my-post/
</code></pre>
テーブルもコードブロックも正しく変換されています。
nl2br を入れているのは、ブログ記事で「意図的な改行」を使うケースがあるためです。Markdown の標準仕様では、改行1つは無視されて段落になります。nl2br を有効にすると、改行1つが <br> に変換されます。
なぜ Python markdown ライブラリか
候補として pandoc・mistune・markdown-it-py なども検討しました。
pandoc は変換品質が高いですが、システムへのインストールが必要で Python だけで完結しません。mistune はシンプルですが、テーブル対応が標準では入っていない(追加パーサーが必要)。markdown-it-py は Node.js 由来で Python 版は機能がやや少ない。
Python 標準の markdown ライブラリは pip install markdown の1コマンドで導入でき、extensions の追加で機能を柔軟に拡張できます。テーブル・コードブロック・改行変換のすべてが公式 extensions でカバーされているのが決め手でした。
pip install markdown
バリデーション ── 投稿前に壊れた記事を止める
parse_post.py には3種類のバリデーションが組み込まれています。
- 必須項目チェック(
validate_fields()) ── 上述の通り - 画像ファイル存在確認(
check_image_exists()) - 制作メモ混入検出(
check_production_meta_contamination())
def check_image_exists(image_path: str):
path = Path(image_path)
return path.exists()
アイキャッチのパスが frontmatter に書かれていても、実際のファイルが存在しなければ「ファイルが見つかりません」というエラーになります。パスのタイポや画像の置き忘れを事前に検出できます。
制作メモ混入検出とは
制作メモ混入検出は、2026年4月21日に実際に起きた公開品質事故がきっかけで作りました。
このとき何が起きたかというと、AI が記事を書いたあと「メタ情報案」「自己チェック」「v2修正メモ」といった制作用の内部情報が、本文の末尾に残ったまま WordPress に公開されてしまいました。読者から見ると「## 自己チェック」「- タイトル案3つ」といったブロックが記事の末尾に表示されてしまう状態です。
これを防ぐため、PRODUCTION_META_PATTERNS というパターンリストで本文を検査し、ヒットした場合は公開を自動ブロックするようにしました。
PRODUCTION_META_PATTERNS = [
r"自己チェック",
r"品質審査前確認",
r"品質審査",
r"メタ情報案",
r"タイトル案",
r"SEOタイトル案",
r"meta\s*description\s*案",
r"ディスクリプション案",
r"v\d+修正", # v2修正, v3修正...
r"修正メモ",
r"制作メモ",
r"編集メモ",
r"構成メモ",
r"タグ候補",
r"カテゴリ候補",
r"スラッグ案",
r"slug案",
]
16パターンあります。検出の仕組みは正規表現で本文を行ごとに走査するシンプルなものです。
def check_production_meta_contamination(body: str):
findings = []
lines = body.splitlines()
for pattern in PRODUCTION_META_PATTERNS:
regex = re.compile(pattern, re.IGNORECASE)
for i, line in enumerate(lines):
if regex.search(line):
snippet = line.strip()
if len(snippet) > 80:
snippet = snippet[:80] + "..."
findings.append((pattern, f"L{i+1}: {snippet}"))
break # 同じパターンは1件で十分
return findings
制作メモが検出された場合の出力例です。
❌ 本文に制作メモが混入しています(公開前に必ず削除してください):
- [自己チェック] L247: ## 自己チェック
- [タイトル案] L252: ### タイトル案(2案)
このメッセージが出たら exit code 2 でスクリプトが止まります。どうしても強制公開したい場合(本文にこれらの言葉が意図的に含まれる記事など)は --allow-meta-keywords オプションで回避できますが、通常は使いません。
コマンド1発で投稿する手順
実際の手順を順番に説明します。
手順1: frontmatter と本文を書いた Markdown ファイルを用意する
drafts/articles/{slug}/ フォルダに 04_draft_v1.md を用意します。
drafts/articles/my-first-post/
├── 04_draft_v1.md ← frontmatter + 本文
├── 05_review_v1.md ← 品質審査(合格マークが必要)
└── images/
└── my-first-post_eyecatch.png
手順2: parse_post.py で単体検証する(任意)
投稿前に Markdown ファイル単体をチェックできます。
python scripts/parse_post.py drafts/articles/my-first-post/04_draft_v1.md
正常な場合の出力:
=== 記事情報 ===
title: はじめての記事タイトル
slug: my-first-post
categories: ['ブログ自動化']
tags: ['Python', 'WordPress']
excerpt: この記事は…
eyecatch: images/my-first-post_eyecatch.png
eyecatch_alt: 作業デスクの様子
=== チェック結果 ===
画像ファイル存在: True
本文文字数: 4523
制作メモ混入チェック: OK
手順3: publish_post.py でフォルダを指定して投稿する
python scripts/publish_post.py drafts/articles/my-first-post/
フォルダを渡すと、publish_post.py は以下を自動で行います。
04_draft_v{N}.mdの最新バージョンを自動検出- 対応する
05_review_v{N}.mdの合格確認(品質審査が通っていなければ止まる) .envの読み込み- Markdown の解析とバリデーション
- 本文を HTML に変換
- カテゴリ・タグを WordPress の ID に解決
- アイキャッチ画像をアップロード
- SEO メタ情報を準備
- REST API で WordPress に投稿
- 公開後の自動検査(ステータス・カテゴリ・タグ・アイキャッチ・SEO・ライブURL)
投稿成功時の出力(抜粋):
モード: 新規公開
入力 : フォルダ構造モード
フォルダ : drafts/articles/my-first-post
対象draft : 04_draft_v1.md
✅ 05_review_v1.md 合格確認済み
1/7 .env を読み込みます...
2/7 Markdown を解析します...
3/7 本文をHTMLに変換します...
4/7 カテゴリ・タグを解決します...
カテゴリID: [12]
タグID: [34, 56]
5/7 アイキャッチ画像をアップロードします...
6/7 SEOメタ情報を準備します...
7/7 WordPress に公開投稿します...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 公開が完了しました
投稿ID : 123
投稿URL : https://example.com/my-first-post/
アイキャッチID : 456
カテゴリ : [12]
タグ : [34, 56]
SEO : ✅ 自動反映済み
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
フォルダ構造モード vs 単一ファイルモード
publish_post.py には2つの入力モードがあります。
フォルダ構造モード(推奨):
python scripts/publish_post.py drafts/articles/{slug}/
フォルダを渡すと find_latest_draft() が最新の 04_draft_v{N}.md を自動検出します。版を上げて再投稿するときも 04_draft_v2.md を置けば自動的にそちらが使われます。品質審査の合格確認(05_review_v{N}.md の存在チェック)も自動で実行されます。
単一ファイルモード(旧仕様・互換維持):
python scripts/publish_post.py drafts/articles/{slug}/04_draft_v1.md
ファイルを直接指定する旧仕様で、品質審査チェックはスキップされます。緊急時や古いスクリプトとの互換が必要な場合に使います。
def resolve_input_path(input_path: Path):
if input_path.is_dir():
md_path, version = find_latest_draft(input_path)
return md_path, input_path, version
elif input_path.is_file():
return input_path, None, None # 単一ファイルモード
else:
raise FileNotFoundError(f"パスが見つかりません: {input_path}")
既存投稿をリライトして更新する場合は --update オプションで投稿 ID を指定します。
python scripts/publish_post.py drafts/articles/{slug}/ --update 3205
詳細はREST API の認証・画像アップロード・カテゴリ解決の詳細はこちらで解説しています。
つまずきやすいポイントと解決策
実際に運用してつまずいたケースを5つ挙げます。どれも「設定に注意」ではなく、具体的に何が起きて何が原因かまで書きます。
1. categories を文字列で書くとエラー
起きたこと: categories: ブログ自動化 のように文字列で書いたら、カテゴリが正しく設定されなかった。
原因: parse_post.py はカテゴリとタグを YAML リスト形式で期待しています。文字列で渡すと list ではなく str として扱われ、REST API へのリクエストが壊れます。
解決策: 必ず YAML リスト記法で書く。
# NG
categories: ブログ自動化
# OK
categories:
- ブログ自動化
タグも同様です。1件だけでも - タグ名 の形にします。
2. YAML クオートの罠:タグがクオート付きで登録される
起きたこと: タグを tags: ["Python", "WordPress"] のようにダブルクオートで囲んで書いたら、WordPress 上で "Python" というタグが新規作成されてしまった。
原因: YAML の値にクオートが含まれたまま 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
ただし、この関数はリスト記法で書かれた各項目に適用されます。インラインリスト記法(["Python"] 形式)は parse_post.py のパーサーが対応していないため、必ずブロックリスト記法を使うことをお勧めします。
3. eyecatch パスの相対・絶対問題
起きたこと: eyecatch: images/my-post_eyecatch.png と書いてコマンドを実行したら「画像が見つかりません」と言われた。
原因: スクリプトはカレントディレクトリ(cwd)を起点にパスを解決します。blog-automation/ ディレクトリから実行していれば blog-automation/images/... を探しますが、Markdown ファイルの隣にある images/ は drafts/articles/{slug}/images/ にあります。
解決策: resolve_image_path() は cwd 起点でパスを解決するため、frontmatter には cwd からの相対パスを書きます。
# blog-automation/ から実行する場合
eyecatch: drafts/articles/my-post/images/my-post_eyecatch.png
# または絶対パスで指定
eyecatch: /Users/haro/blog-automation/drafts/articles/my-post/images/my-post_eyecatch.png
実際には drafts/articles/{slug}/images/{slug}_eyecatch.png のフォルダ構造に統一して、常に blog-automation/ ディレクトリから実行するルールにしています。
4. SEOメタが反映されない
起きたこと: seo_title と seo_description を frontmatter に書いたが、WordPress の SEOPress で何も設定されていなかった。
原因: WordPress の REST API は、デフォルトではカスタムメタフィールドを受け付けません。PHP 側で register_post_meta() を使って対象フィールドを公開許可する設定が必要です。
解決策: WP Code プラグイン(または functions.php)に以下のコードを追加します。
// SEOPress のメタフィールドを REST API に公開する
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 SEO の場合は _yoast_wpseo_title と _yoast_wpseo_metadesc を登録します。この設定をしていない場合、スクリプト実行後に「⚠️ 手動設定が必要」と表示され、管理画面での手動入力ガイドが出ます。
5. テーブルが変換されない
起きたこと: Markdown に | で書いたテーブルが、WordPress 上では | 文字がそのまま表示されてしまった。
原因: Python markdown ライブラリはデフォルトでテーブルに対応していません。extensions に "tables" を追加しないとテーブルが変換されません。
解決策: simple_markdown_to_html() に "tables" が含まれているか確認します。
extensions = [
"tables", ← これが必要
"nl2br",
"fenced_code",
]
pip install markdown でインストールした標準のライブラリには tables extension が含まれています。追加インストールは不要です。
REST API の全般的なつまずき(認証エラー・403・タイムアウト等)についてはREST API 全般のつまずき集はこちらにまとめます。
よくある質問
Q: frontmatter のカテゴリやタグに日本語を使っても大丈夫ですか?
YAML の文字列として日本語は問題なく使えます。ただし WordPress REST API のカテゴリ解決は name の完全一致で行います。WordPress に登録されているカテゴリ名と frontmatter の記述が1文字でも違うと「カテゴリが見つかりません」エラーになります。
全角スペースが混入している、中点(・)と半角ハイフンが混在しているといった表記ゆれが原因で一致しないケースが実際にありました。resolve_term_ids() は全件取得してローカルで完全一致検索するので、WordPress の検索トークナイズ問題は回避できますが、記述の正確さが前提です。
Q: Gutenberg エディターで開くと Markdown が崩れませんか?
publish_post.py は Markdown を HTML に変換してから WordPress に送信します。Gutenberg が受け取るのは変換後の HTML です。Gutenberg のブロック分割とは別の仕組みで、投稿が WordPress に保存されると「クラシックブロック」として表示されます。
Markdown 記法がそのまま Gutenberg に表示されることはありません。Gutenberg で記事を開いて編集したい場合は、クラシックブロックをブロックに変換するか、HTML として直接編集することになります。
Q: SEOプラグインを使っていない場合はどうなりますか?
seo_title と seo_description を frontmatter に書いても、対応プラグインがなければ反映されません。ただし、記事の公開自体には何も影響しません。SEO 以外の設定(タイトル・カテゴリ・タグ・アイキャッチ・本文)は正常に投稿されます。
.env の WP_SEO_PLUGIN を none に設定するか、seo_title / seo_description を frontmatter から省略すれば、SEOメタに関する処理がスキップされます。
まとめ ── Markdown を「書いたら投稿」の仕組みにする
この記事で扱った内容を整理します。
- frontmatter の設計:
parse_post.pyのREQUIRED_FIELDS(7項目)をすべて記述する。カテゴリ・タグは YAML リスト形式で書く - Markdown→HTML 変換:Python
markdownライブラリをtables/nl2br/fenced_codeextension つきで使う - バリデーション:必須項目チェック・画像ファイル存在確認・制作メモ混入検出の3種類が自動実行される
- コマンド1発で投稿:
python scripts/publish_post.py drafts/articles/{slug}/でフォルダ指定
この仕組みの中で、parse_post.py が担うのは「投稿する前に壊れた記事を止める」役割です。記事を書いてコマンドを実行したとき、管理画面を一切開かずに投稿が完了する快適さは、この検証ステップがあってこそ成立しています。
REST API 認証・画像アップロード・自動検査の詳細はWordPressへのWP自動投稿の詳細はこちらで解説しています。自動化の全体像はブログ自動化5ステップの全体像はこちらからどうぞ。
仕組みを整えてしまえば、記事を書くことに集中できます。それだけでブログ更新のハードルが下がります。あなたのブログ運営の「投稿作業」は、今どのくらい時間がかかっていますか。
parse_post.py の全コード(バリデーション設計・SEOプラグイン連携の詳細含む)は note で公開しています。ブログ自動化の試行錯誤の記録も合わせてまとめているので、仕組みの背景を知りたい方はあわせて読んでみてください。
関連 note 記事
publish_post.py の全コード・実エラー・解決過程は note の有料記事で公開しています(500円)。
