WordPress で記事を週1本以上入れていると、原稿を編集画面へコピペし、見出しの改行や表の崩れを毎回手で直す作業が重くなります。
AIに任せるのは下書き投入までにします。確認と予約投稿は人が持ちます。人は、プレビューで見る場所をチェックリストに絞ります。
DP JOURNAL は WordPress で運用しているオウンドメディアです。2026年9月時点で公開記事は40本を超えています。2026年8月中旬から9月中旬のおよそ4週間半では、14本を公開しました。
この本数になると、見出しの改行、表の崩れ、要約欄の余計な記号を、人が毎回直すことになります。原稿を書く時間とは別に、入稿だけで集中力を使います。
私たちは、Markdown 原稿を WordPress に下書きで入れるところまでを Claude Code とスクリプトに任せています。確認と予約投稿は人が持ちます。この記事も、その流れで入稿しています。
原稿と画像がそろっている前提で、原稿が固まってから下書き投入・確認・予約までが、1本あたり10分ちょっとです。原稿を書く時間は含みません。
この運用を含めて、社内でAIを使う仕組みを4か月で作った記録はこちら
WordPressの記事入稿自動化は、原稿から予約投稿まで5つの工程で考える
私たちの入稿フローは、次の5つです。
原稿の正本は Markdown に置きます。WordPress の編集画面で直接直し続けると、どちらが最新か分からなくなります。直すときは Markdown 原稿を直し、もう一度変換して入れ直す運用にしています。
スクリプトからは公開を送りません。REST API で投稿を作るときも、status=draft に固定します。公開や予約の判断は編集責任者が持ちます。
よくある失敗は、便利だからといって公開までスクリプトに含めることです。本文の崩れや内部リンクの404は、公開後に気づくと読者に見えてしまいます。下書きまでで止める設計にしておくと、人が見る余地を残せます。
準備:アプリケーションパスワードと権限
WordPress REST API に投稿を作るには、認証が必要です。私たちは、アプリケーションパスワードを使います。アプリケーションパスワードは WordPress 5.6 で導入された仕組みで、連携ごとに個別に発行し、個別に取り消せます。
投入に使うユーザーの権限は、必要な範囲に絞るのが基本です。投稿の作成・予約だけなら編集者(Editor)で足ります。管理者のアプリケーションパスワードは、漏れたときの被害が大きくなります。
ユーザー名とアプリケーションパスワードは、コードに書きません。環境変数で渡します。リポジトリや共有フォルダに認証情報が残ると、後から消すのが面倒になります。
あわせて、REST API のユーザー一覧が未ログインで見える設定になっていないかも確認します。未ログインの状態で https://example.com/wp-json/wp/v2/users を開き、ユーザー名が見えないかを確認します。
準備がうまくいったかは、認証付きのリクエストで下書き作成ができるかを見ます。失敗する場合は、ユーザー名、アプリケーションパスワード、権限、REST API の認証方式を順に確認します。
よくある失敗は、テスト用に管理者の認証情報を使い、そのまま運用に残すことです。もうひとつは、パスワードをスクリプトに直書きすることです。どちらも、後から連携を止めにくくなります。
原稿の書き方:frontmatter に入稿情報を持たせる
Markdown 原稿の先頭には、frontmatter を置きます。frontmatter は、本文とは別にメタ情報を書くための領域です。タイトル、スラッグ、要約、カテゴリ、タグを、本文と同じファイルで管理します。
---
記事タイトル: 記事のタイトル
slug: example-article-slug
excerpt: 記事一覧や検索結果に出る要約文(80〜120字)
カテゴリ: コラム
タグ: [タグA, タグB]
---
この形にしておくと、入稿時に「タイトルはどこに書いたか」「要約は別ファイルだったか」と探さなくて済みます。原稿を直すときも、本文と入稿情報を一緒に見直せます。
本文に入れない編集メモがある場合は、投入前に除きます。frontmatter も本文からは外します。WordPress に送る本文は、読者に見える内容だけにします。
うまくいっているかは、変換前の段階で確認します。frontmatter の開始と終了がそろっているか、タイトルやスラッグが空になっていないかを見ます。要約欄に入れる文は、WordPress の抜粋として見えてよい形にします。
よくある失敗は、excerpt を引用符で囲み、その引用符が要約欄に残ることです。スクリプト側で剥がす処理を入れていない場合、引用符ごと入ります。原稿の書き方をそろえるか、投入前に取り除く処理を入れます。
カテゴリやタグは、名前ではなくタームのID(整数)を送ります。名前しか手元にない場合は、GET /wp-json/wp/v2/categories?search=<名前> でカテゴリを引きます。タグは /wp-json/wp/v2/tags?search=<名前> で引き、IDに変換してから送ります。
WordPressへの下書き投入:Markdownを変換してstatus=draftで入れる
Markdown から HTML への変換には pandoc を使います。pandoc は既定で72桁で行を折り返します。私たちは、見出しや段落の途中に折り返し改行を入れないために --wrap=none を付けています。
変換前には、frontmatter と編集メモを本文から除きます。先頭の --- で囲まれたブロックを取り除いた一時ファイルを作り、それを pandoc に渡します。frontmatter の中身は、入稿情報(title/slug/excerpt/カテゴリ/タグ)として別に読みます。編集メモは原稿に “ の形で書いておき、同じ前処理で取り除きます。
import re
from pathlib import Path
source = Path("article.md").read_text(encoding="utf-8")
body = re.sub(r"\A---\s*\n.*?\n---\s*\n", "", source, flags=re.DOTALL)
body = re.sub(r"", "", body, flags=re.DOTALL)
Path("article.body.md").write_text(body, encoding="utf-8")
変換コマンドは次の形です。
pandoc article.body.md -f markdown-native_divs -t html --wrap=none -o article.html
--wrap=none は、見出しや段落の途中に折り返し改行を入れない指定です。-f markdown-native_divs は、原稿に書いた <div> を pandoc の Div として解釈させないために付けています。表を囲むタグをそのまま扱いたいときに効きます。
変換後には、HTML を機械でチェックします。<div と </div> の数が一致するか、見出しタグの中に改行や <br> が残っていないかを見ます。
下書き投入は、REST API の POST /wp-json/wp/v2/posts に送ります。title、slug、excerpt、content、categories、tags、status=draft を送る形です。認証はアプリケーションパスワードの Basic 認証を使い、値は環境変数から読みます。
以下は、ダミーのサイト URL を使った Python の例です。article.html を本文として送り、下書きを作ります。カテゴリとタグは環境変数で渡す形にしています。requests を使うため、先に pip install requests を実行します。このコードは post_draft.py として保存します。
import base64
import json
import os
import re
import sys
from pathlib import Path
import requests
SITE_URL = os.environ.get("WP_SITE_URL", "https://example.com").rstrip("/")
WP_USER = os.environ["WP_USER"]
WP_APP_PASSWORD = os.environ["WP_APP_PASSWORD"]
TITLE = os.environ["WP_POST_TITLE"]
SLUG = os.environ["WP_POST_SLUG"]
EXCERPT = os.environ["WP_POST_EXCERPT"]
CATEGORY_IDS = json.loads(os.environ.get("WP_CATEGORY_IDS", "[]"))
TAG_IDS = json.loads(os.environ.get("WP_TAG_IDS", "[]"))
HTML_PATH = Path(os.environ.get("WP_HTML_PATH", "article.html"))
def read_html(path: Path) -> str:
if not path.exists():
raise FileNotFoundError(f"HTML file not found: {path}")
return path.read_text(encoding="utf-8")
def check_html(html: str) -> None:
open_divs = len(re.findall(r"<div\b", html, flags=re.IGNORECASE))
close_divs = len(re.findall(r"</div>", html, flags=re.IGNORECASE))
if open_divs != close_divs:
raise ValueError(f"div tag count mismatch: open={open_divs}, close={close_divs}")
headings = re.findall(
r"<h[1-6][^>]*>(.*?)</h[1-6]>",
html,
flags=re.IGNORECASE | re.DOTALL,
)
for heading in headings:
if "\n" in heading or re.search(r"<br\s*/?>", heading, flags=re.IGNORECASE):
raise ValueError("newline or br found inside a heading tag")
def build_headers() -> dict:
token = base64.b64encode(f"{WP_USER}:{WP_APP_PASSWORD}".encode("utf-8")).decode("ascii")
return {
"Authorization": f"Basic {token}",
"Content-Type": "application/json",
}
def create_draft(html: str) -> dict:
payload = {
"title": TITLE,
"slug": SLUG,
"excerpt": EXCERPT.strip().strip('"'),
"content": html,
"categories": CATEGORY_IDS,
"tags": TAG_IDS,
"status": "draft",
}
response = requests.post(
f"{SITE_URL}/wp-json/wp/v2/posts",
headers=build_headers(),
data=json.dumps(payload),
timeout=30,
)
if response.status_code >= 400:
raise RuntimeError(f"WordPress API error: {response.status_code} {response.text}")
return response.json()
def main() -> None:
html = read_html(HTML_PATH)
check_html(html)
created = create_draft(html)
returned_content = created.get("content", {}).get("rendered", "")
if returned_content:
check_html(returned_content)
print(json.dumps({
"id": created.get("id"),
"status": created.get("status"),
"slug": created.get("slug"),
"link": created.get("link"),
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
try:
main()
except Exception as error:
print(f"ERROR: {error}", file=sys.stderr)
sys.exit(1)
実行前に、必要な環境変数を渡します。値は自社サイト用に置き換えます。認証情報は、コマンド履歴や共有ファイルに残さない運用にしてください。
export WP_SITE_URL="https://example.com"
export WP_USER="your-wordpress-user"
export WP_APP_PASSWORD="your-application-password"
export WP_POST_TITLE="記事のタイトル"
export WP_POST_SLUG="example-article-slug"
export WP_POST_EXCERPT="記事一覧や検索結果に出る要約文"
export WP_CATEGORY_IDS="[]"
export WP_TAG_IDS="[]"
export WP_HTML_PATH="article.html"
python post_draft.py
うまくいったかは、出力された status が draft になっているかで確認します。WordPress の投稿一覧でも、下書きとして作られているかを見ます。プレビューを開き、本文が入っているかも確認します。
よくある失敗は、投入前の HTML だけ見て安心することです。WordPress に入った後の本文でも、同じチェックをかけます。返ってきた本文で見出し内の改行や <br>、div の数を確認すると、変換後と投入後の差に気づけます。
人が見るのはここだけ:下書き確認のチェックリスト
下書きが入ったら、人がプレビューを見ます。見る場所は絞ります。毎回すべてを読み直すのではなく、機械で崩れやすい場所と、公開判断に関わる場所を確認します。
誰が確認して誰が公開を決めるかの線引きは、SNS運用の承認フローと同じ考え方です
確認する項目は次のとおりです。
見出しは、番号と本文が離れていないかを見ます。表は、横幅が広いときにスマホ幅で表示が崩れやすい要素です。プレビューを狭い幅でも確認します。
要約欄は、本文より見落としやすい場所です。frontmatter の引用符や記号が残っていないかを見ます。記事一覧や検索結果に出る文として不自然でないかも確認します。
記事本文の下も見ます。本文だけが正しく見えていても、閉じタグの不整合でサイドバーや新着一覧が崩れることがあります。ページ下部までスクロールし、幅いっぱいに広がっていないかを確認します。
うまくいった状態は、本文、周辺パーツ、要約欄、内部リンクがすべて意図どおり見える状態です。確認が終わったら、予約投稿へ進みます。
よくある失敗は、本文の上部だけ見て予約することです。表や記事下部の崩れは、最後まで見ないと気づけません。チェックリストにしておくと、確認者が変わっても見る場所をそろえられます。
WordPress REST APIで予約投稿し、内部リンクの公開順をそろえる
確認が済んだら、予約投稿にします。REST API では、投稿の status に future を指定できます。公開日時は date で指定できます。管理画面から予約しても構いません。
私たちの運用では、確認して予約を押すのは編集責任者です。スクリプトは下書きまでです。予約や公開の判断を人が持つことで、最後に内部リンクや表示崩れを確認できます。
予約投稿で特に気をつけるのは、内部リンクの公開順です。リンク先が予約中の記事だと、リンク元が先に公開された時点で404になります。公開済みの記事にだけリンクを張るか、リンク先を先に公開する順に並べます。
後から公開する記事へリンクしたい場合は、いったん入れません。リンク先の記事が公開された後に、張り返します。私たちは実際に内部リンクで404を出したことがあるため、いまはこの運用にしています。
うまくいったかは、予約済みの記事のリンク先を確認します。プレビュー上でリンクを開き、公開済みの記事だけに向いているかを見ます。未公開の記事へ向いている場合は、公開後に張り返すメモを残します。
私たちがつまずいた4つのこと
見出しの番号と本文が改行で割れた
最初につまずいたのは、見出しの途中に改行が入ることでした。見出しの番号と本文が分かれて見えました。原因は、pandoc の既定の折り返し改行を WordPress の自動整形が <br> にしていたことです。
直し方は、変換時に --wrap=none を付けることでした。さらに、投入後に見出し内の改行を機械でチェックするようにしました。人がプレビューで気づく前に、スクリプトで止めるためです。
確認方法は、変換後の HTML と WordPress から返ってきた本文の両方を見ることです。<h2> や <h3> の中に改行や <br> が残っていないかを確認します。プレビューでは、見出しの番号と本文が同じ流れで読めるかを見ます。
要約欄の前後にダブルクォートが入った
公開後に、要約欄の前後にダブルクォートが入っていることに気づきました。原因は、原稿の frontmatter に入れた引用符を投入スクリプトが剥がしていなかったことです。本文ではなく、抜粋側の問題でした。
直し方は、投入スクリプトで要約の前後にある引用符を取り除くことでした。同じ症状だった公開済みの13本も一括で直しました。公開済みの記事にも影響が出るため、変換や投入の設定変更後は過去分も見ます。
確認方法は、投稿本文だけでなく、要約欄を見ることです。記事一覧や検索結果に出る文に、余計な引用符や記号が混ざっていないかを確認します。本文プレビューだけでは気づけない場合があります。
表を囲む div の扱いで記事下部が崩れた
記事本文の下で、サイドバーと新着一覧が画面幅いっぱいに広がったことがありました。原因は、表を囲む <div> を pandoc が自分の Div として解釈し、文書の末尾に閉じタグを余分に出していたことです。本文の途中ではなく、記事下部に影響が出ました。
直し方は、変換コマンドに -f markdown-native_divs を付けることでした。さらに、投入前に <div と </div> の数を比べるようにしました。数が合わなければ、下書き投入の前に止めます。
確認方法は、HTML のタグ数を見ることと、プレビューで記事の下まで見ることです。表の直後だけでなく、サイドバーや新着一覧の表示も確認します。ページ下部の崩れは、本文の読み直しだけでは見落とします。
修正前に予約した記事で同じ崩れが再発した
div の問題を直した2日後に、同じ崩れが別の記事で再発しました。原因は、修正前に作って予約投稿に積んでいた記事が、壊れたまま公開日を迎えたことです。変換設定の修正は、これから入れる記事にしか効きません。
それ以来、変換の設定を触ったら、公開・予約・下書きの全記事を検算しています。すでに WordPress に入っている記事も対象です。予約中の記事は特に見落としやすいため、公開前に確認します。
確認方法は、現在の下書きだけでなく、予約済みの記事も同じチェックに通すことです。見出し内改行、div の数、要約欄、記事下部を見ます。設定を直した後ほど、過去に入れた記事を疑います。
運用して分かったこと
原稿と画像がそろっている前提で、原稿が固まってから下書き投入・確認・予約までが、1本あたり10分ちょっとです。原稿を書く時間は含みません。手作業でコピペ入稿していた頃との比較は、測っていないため書けません。
時間が短くなった理由は、すべてを機械に任せたからではありません。機械に任せる場所と、人が見る場所を分けたからです。人が毎回迷う範囲を、チェックリストに絞りました。
特に効いたのは、崩れやすい場所を先に決めたことです。見出しの改行、表を囲むタグ、要約欄は、起きる問題が決まっていました。決まっている問題は、投入前後に機械で検出できます。
一方で、予約投稿や公開判断は人が持っています。内部リンクの公開順、本文とアイキャッチの合い方、カテゴリやタグの意図は、最後に人が見る必要があります。私たちは、ここを自動で通す運用にはしていません。
制作を外部に委託する場合に、入稿や確認の範囲を仕様書でどう決めるか
未検証の環境もあります。WordPress.com やヘッドレス構成で同じ流れがそのまま動くかは未検証です。導入する場合は、公式情報で確認してください。
最初の1本でやること
最初から運用全体を作り込む必要はありません。まずは、Markdown 原稿を HTML に変換し、WordPress に status=draft で入れるところだけを自動化します。公開や予約は、これまでどおり人が操作します。
最初の1本では、frontmatter を用意し、本文用の一時ファイルを作ってから pandoc article.body.md -f markdown-native_divs -t html --wrap=none -o article.html で変換します。投入前に div の数と見出し内改行を確認します。下書き投入後も、返ってきた本文で同じ確認をします。
そのうえで、プレビューをチェックリストに沿って見ます。見出し、表、要約欄、記事下部、内部リンク、アイキャッチ、タイトル・スラッグ・カテゴリ・タグを確認します。問題がなければ、人が予約投稿に進みます。
変換の設定を直したら、これから入れる記事だけでなく、公開済み・予約済み・下書きの記事も検算します。すでに予約に積んだ記事には、修正後の設定が自動では反映されません。ここを忘れると、同じ崩れが公開時に出ます。
社内でAIを使った運用の仕組みづくりを相談したい方は、お問い合わせからご相談ください。記事制作、入稿、確認、予約投稿までの流れを整理し、自社で回せる形にするところから一緒に進めます。
参照した資料
WordPress アプリケーションパスワード
https://developer.wordpress.org/advanced-administration/security/application-passwords/
WordPress REST API の認証
https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
WordPress REST API 投稿エンドポイント
https://developer.wordpress.org/rest-api/reference/posts/
WordPress REST API カテゴリエンドポイント
https://developer.wordpress.org/rest-api/reference/categories/
pandoc Manual
https://pandoc.org/MANUAL.html