AIが書いた原稿をWordPressへ自動で入れるときは、自動化の終点を「公開」ではなく「下書き登録」に固定します。AI出力をJSON Schema(データの構造と制約を定義したもの)で検証し、REST API(プログラムからWordPressを操作するためのHTTP経由の受け口)へ status=draft を定数として渡してPOSTするのが基本です。さらに、認証ユーザーから公開権限を外し、コードの誤変更があっても公開できない状態にします。内容の確認と公開判断は、運営者側に残す必要があります。
この記事では、AI出力を受けるJSON Schema、Application Passwords(アプリケーション用の専用パスワード)でREST APIへ投げるNode.jsの最小実装、同じ原稿の二重登録を抑える方法と限界、そして人間承認と監査ログに残すべき項目までを一続きで解説します。記事本文そのものの書き方は扱いません。
情報確認日:2026年7月28日(日本時間)
結論:終点を下書き登録に置き、statusはコード側の定数にする
この記事の結論
- 自動化の範囲は「生成 → 検証 → 下書き登録」まで。公開は人の操作で行う
statusはAIの出力項目に含めず、登録コード側で"draft"に固定する- 認証はApplication Passwordsを使い、
publish_postsを持たない専用ユーザーで発行する - AI出力はJSON Schemaで検証し、通らなかったものはWordPressへ送らずqueue(処理待ちの列)へ戻す
- 単一workerではslugの事前照会・post metaのkey・ローカル台帳を重ねる。複数workerではサーバー側の排他制御が別途必要
- 確認者・修正差分・承認日時を残さないと、公開物の責任の所在が消える
自動化の終点を「下書き登録」にする
生成・検証・登録・承認を分離する
AI下書きの自動化がうまくいかない典型は、1つのスクリプトが「原稿を生成して、そのままWordPressへ投げて、ついでに公開する」という作りになっている状態です。途中で失敗したときにどこまで進んだのか分からず、再実行すると同じ記事が増えます。工程を次の4つに割り、それぞれが独立して再実行できる形にします。
- 生成:AIに原稿を書かせ、結果をJSONファイルとして保存する。この段階ではWordPressに触れない
- 検証:保存したJSONをschemaに通し、必須fieldと文字数と許可HTMLを確認する。失敗したものは登録へ進めない
- 登録:検証を通ったものだけを
status=draftでPOSTする。ここが自動化の終点 - 承認:人が管理画面で読み、修正し、公開ボタンを押す。ここは自動化しない
分離の実利は再実行のしやすさに出ます。登録で500エラーが返ったとき、生成からやり直すとAIの出力が変わってしまい、何を直したのか追えなくなります。生成物がファイルとして残っていれば、登録工程だけを同じ入力で再試行できます。工程ごとに成果物をファイルに落とす設計は、後述する監査ログの材料にもそのままなります。
工程の切れ目には、次へ渡すための合格条件を明示します。生成から検証へは「JSONとしてパースできたか」、検証から登録へは「schema・HTML・業務ルールをすべて通ったか」、登録から承認へは「post IDとstatus=draftを応答で確認できたか」です。各判定結果を構造化して記録すれば、複数の検査があっても停止理由を追跡できます。
publish を自動化しない理由
WordPress REST APIの投稿作成では、status に publish、future、draft、pending、private を指定できます。技術的には publish を渡せば即座に公開されます。それでも下書きで止めるのは、次の3つが機械に委ねられない責任だからです。
- 事実確認:AIは書けなかった事実を書けたかのように埋めます。数値・日付・製品名・仕様は、一次情報に当たって確かめた人がいてはじめて記事に載せられます
- 表記と文脈:サイト内の用語統一、既存記事との重複、リンク先の妥当性は、そのサイトを運営している人にしか判断できません
- 権利・規制上のリスク:誤情報、第三者の権利、広告表示、業界固有の規制などは、適用される法令・契約・社内基準に照らして人が判断します。必要な分野では専門家の確認も組み込みます
Google検索セントラルの「役立つ、信頼できる、ユーザー第一のコンテンツの作成」は、コンテンツを「誰が」「どのように」「なぜ」作ったかという観点を示しています。AIや自動化を大幅に使った場合、読者が制作方法を知りたいと合理的に考えられる場面では、開示や背景説明を検討するよう案内しています。下書き、根拠、修正差分、確認者を残すと、制作過程を必要に応じて説明しやすくなります。
実装上の帰結は単純です。AIの出力schemaに status というfieldを置かないこと、そして登録コードでは status: "draft" をリテラルとして書くことです。payload(送信するデータ本体)から status を受け取れる作りにしてしまうと、プロンプトの改変や出力の揺れが、そのまま意図しない公開に直結します。承認そのものの運用設計は「AI記事の編集承認ワークフロー」で扱っています。
WordPress REST APIの準備をする
最小権限の認証方法を選ぶ
WordPress REST APIの認証には主に3つの選択肢があり、外部スクリプトから叩く用途で選ぶべきものは決まっています。
| 認証方式 | 仕組み | この用途での可否 |
|---|---|---|
| Cookie認証 | 管理画面にログイン中のCookieを使う。nonce(使い捨てトークン)を X-WP-Nonce ヘッダーか _wpnonce で渡す必要がある |
不可。公式ドキュメントも「WordPressの内部でREST APIを使い、現在のユーザーがログインしている場合にのみ適用できる」と限定している |
| Application Passwords | WordPress 5.6で追加された機能。ユーザー編集画面でアプリごとに専用パスワードを発行し、Basic認証(RFC 7617)で送る | これを使う。個別に失効でき、通常のログインパスワードを渡さずに済む |
| Basic Authプラグイン | 通常のログインID/パスワードをBasic認証で受け付けるプラグイン | 不可。公式に「開発とテストにのみ使用すべきで、本番環境では使用しない」と明記されている |
Application Passwordsには押さえておくべき仕様がいくつかあります。既定ではHTTPSで処理されるリクエストで利用でき、HTTPではBasic認証の資格情報を通信途中で見られる危険があります。発行される値は画面上で読みやすく区切って表示されますが、スペースの有無どちらでも認証できます。また、この値ではwp-login.phpから対話的にログインできません。連携ごとに別のApplication Passwordを発行し、不要時や漏えいの疑いがあるときは個別に失効させます。
Application Password自体に独立した権限scopeが付くわけではなく、紐づくWordPressユーザーの権限で動きます。管理者や投稿者(Author)はpublish_postsを持つため、この用途には広すぎます。既定roleなら寄稿者(Contributor)、またはreadとedit_postsなど必要なcapabilityだけを持つ専用roleを検討してください。寄稿者は自分の投稿を作成・管理できますが公開できません。実際のroleやプラグインによる権限変更は、導入先で確認します。
発行した値は秘密情報の管理サービスやCIのsecretに置き、リポジトリ、ログ、エラー通知へ出しません。環境変数を使う場合も、プロセス一覧、デバッグ出力、クラッシュレポートへの露出を確認します。最終使用日時と最終使用IPは運用確認の手掛かりになりますが、それだけで不正利用の有無を断定せず、定期的にローテーションしてください。
投稿作成endpointをテストする
投稿の作成は POST /wp-json/wp/v2/posts です(endpointはAPIの受け口となるURLのこと)。実装に入る前に、コマンドラインで3つだけ確認しておくと、後の切り分けが一気に楽になります。
# 1. 認証が通るか(自分のユーザー情報が返れば成功)
curl --user "USERNAME:xxxx xxxx xxxx xxxx xxxx xxxx" \
"https://example.com/wp-json/wp/v2/users/me?context=edit"
# 2. 下書きを1件だけ作れるか
curl -X POST --user "USERNAME:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{"title":"接続テスト","content":"本文","status":"draft"}' \
"https://example.com/wp-json/wp/v2/posts"
# 3. 作った下書きが読めるか
# statusを省くとpublishしか返らないため、明示とcontext=editが要る
curl --user "USERNAME:xxxx xxxx xxxx xxxx xxxx xxxx" \
"https://example.com/wp-json/wp/v2/posts?status=draft&context=edit&_fields=id,slug,status"
3番目が重要です。投稿一覧のstatusパラメータは既定値がpublishなので、何も指定しないと下書きは返りません。status=draftとcontext=edit(編集用の文脈)を指定し、下書きを編集できるユーザーで認証します。接続テストで作った投稿はpost IDを記録し、確認後に管理画面から削除してください。
上のcurl例に実際のApplication Passwordを直接書くと、shell履歴やプロセス情報に残る場合があります。例の値はplaceholderのまま読み、実運用では利用中のsecret管理基盤から安全に渡し、コマンド、標準出力、CIログをマスクしてください。
レスポンスを軽くするグローバルパラメータも覚えておくと役立ちます。_fields は返すfieldを絞るもので、公式には「レスポンス内のfieldの一部だけを返すようWordPressに指示するには _fields クエリパラメータを使えます」と説明されています。重複確認のようにIDとslugしか要らない照会では、本文まで受け取る必要はありません。
AI出力を投稿用schemaへ揃える
必須fieldと許可HTMLを定義する
AIの出力をそのままWordPressへ渡すと、fieldが1つ欠けていたり、想定外のkeyが混ざったりします。間にJSON Schemaによる検証を挟み、通らないものは登録工程へ進めません。次は出発点となるschemaです。文字数、category、HTML規則はサイトの編集基準に合わせて変更してください。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "wp-draft-payload",
"type": "object",
"additionalProperties": false,
"required": ["source_run_id", "title", "slug", "excerpt", "content", "category_slug", "sources"],
"properties": {
"source_run_id": {
"type": "string",
"minLength": 8,
"description": "生成ジョブの一意ID。idempotency keyの材料にする"
},
"title": { "type": "string", "minLength": 10, "maxLength": 60 },
"slug": {
"type": "string",
"maxLength": 72,
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"excerpt": { "type": "string", "minLength": 80, "maxLength": 140 },
"content": { "type": "string", "minLength": 2000 },
"category_slug": { "type": "string", "enum": ["ai", "programming", "tools"] },
"sources": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["url", "checked_at"],
"properties": {
"url": { "type": "string", "format": "uri" },
"checked_at": { "type": "string", "format": "date" }
}
}
}
}
}
このschemaで意図的にやっていることが2つあります。1つはadditionalProperties: falseです。AIがstatusやauthorを付けてきても検証エラーになります。もう1つはstatusというfieldを定義していないことです。なお、format: "uri"とformat: "date"を実際に拒否条件として使うには、利用するvalidatorでDraft 2020-12とformat検証を有効にします。実装によってはformatが注釈として扱われるため、無効なURLや日付を自動的に拒否するとは限りません。
本文のHTMLは、schemaの型検証だけでは守れません。実績のあるHTML sanitizerを使い、許可する要素、属性、URL schemeを明示します。たとえばaを許可しても、hrefに危険なschemeが入らないか、target="_blank"なら適切なrelが付くかを別に確認します。正規表現だけでHTMLを無害化しないでください。WordPress側でもユーザー権限に応じてHTMLがfilterされるため、POST後のcontent.rawを取得し、送信前後の差分を監査ログへ残します。
category・slug・excerptを検証する
schemaを通っても、WordPress側の実体と噛み合わなければ登録は失敗します。投稿作成の categories は「categoryタクソノミーにおいて投稿に割り当てられた語」を表す配列で、渡すのはslugではなくtermのIDです。AIにIDを出力させるのは事故のもとなので、schemaではslugを受け取り、登録の直前に GET /wp-json/wp/v2/categories?slug=ai でIDへ解決します。返る配列の要素数が1件でなければ、そこで止めます。存在しないcategoryを自動作成する作りにすると、綴り間違いのcategoryが増え続けます。
slugは「その投稿タイプ内で一意な英数字の識別子」です。REST APIの投稿作成処理はdraft/pendingでも公開時と同じ基準で一意なslugを生成するため、重複したslugには-2のような接尾辞が付き得ます。同じ原稿を2回POSTしても重複エラーになるとは限らず、下書きが2本できます。このため、登録側で重複抑止を用意します。
excerpt(抜粋)は文字数の上下限に加えて、内容の重複を見ます。titleをそのまま繰り返しただけの抜粋は、一覧ページでも検索結果でも情報量がありません。類似度の閾値は日本語の分かち書きや正規化方法で結果が変わるため、実データで誤検知を確認して決めます。検証の失敗は理由付きで記録し、どのルールで何件落ちたかを後から数えられるようにしておきます。事実そのものの確認手順は「AIのファクトチェック手順」にまとめています。
下書きの重複を抑えて登録する
status=draft でPOSTする
ここまでの設計をNode.jsの最小実装にしたものが次のコードです。schema検証とHTMLのsanitizeを通したpayloadを受け取る前提です。2026年7月時点でサポート中のLTSであるNode.js 22または24を使い、標準のfetchとAbortSignal.timeoutを利用します。実運用ではLTS内でも最新のsecurity releaseへ更新してください。
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const BASE = process.env.WP_BASE_URL; // 例: https://example.com
const USER = process.env.WP_USER; // WordPressのユーザー名
const APP_PW = process.env.WP_APP_PASSWORD; // Application Password
const LEDGER = "./.wp-draft-ledger.json"; // 登録済みkeyの台帳
if (!BASE || !USER || !APP_PW) {
throw new Error("WP_BASE_URL / WP_USER / WP_APP_PASSWORD are required");
}
if (new URL(BASE).protocol !== "https:") {
throw new Error("WP_BASE_URL must use HTTPS");
}
// Application Passwordはスペースの有無を問わないが、送る前に落としておく
const AUTH = "Basic " + Buffer.from(
`${USER}:${APP_PW.replace(/\s+/g, "")}`
).toString("base64");
async function wp(path, init = {}) {
const res = await fetch(new URL(path, BASE), {
...init,
signal: init.signal ?? AbortSignal.timeout(15_000),
headers: {
Authorization: AUTH,
"Content-Type": "application/json",
...init.headers,
},
});
const body = await res.text();
if (!res.ok) {
// Authorization headerやpayload全体はログへ出さない
throw new Error(
`${init.method ?? "GET"} ${path} => ${res.status} ${body.slice(0, 500)}`
);
}
return body ? JSON.parse(body) : null;
}
// WordPressへ送る同じ内容からは同じ鍵ができる
const keyOf = (p) => createHash("sha256")
.update(JSON.stringify({
source_run_id: p.source_run_id,
title: p.title,
slug: p.slug,
excerpt: p.excerpt,
content: p.content,
category_slug: p.category_slug,
}))
.digest("hex")
.slice(0, 32);
async function loadLedger() {
try {
const value = JSON.parse(await readFile(LEDGER, "utf8"));
if (!value || Array.isArray(value) || typeof value !== "object") {
throw new Error("ledger must be a JSON object");
}
return value;
} catch (error) {
if (error?.code === "ENOENT") return {};
throw error; // 壊れた台帳や権限エラーを「空」とみなさない
}
}
async function resolveCategoryId(slug) {
const rows = await wp(
`/wp-json/wp/v2/categories?slug=${encodeURIComponent(slug)}&_fields=id,slug`
);
if (rows.length !== 1) throw new Error(`category not resolvable: ${slug}`);
return rows[0].id;
}
// statusを省くとpublishしか返らない。全statusを見るには認証とcontext=editが要る
async function findBySlug(slug) {
const rows = await wp(
`/wp-json/wp/v2/posts?slug=${encodeURIComponent(slug)}`
+ `&status=any&context=edit&_fields=id,slug,status,meta`
);
return rows[0] ?? null;
}
// この最小例は同時に1件だけ処理するsingle-worker用
export async function createDraft(payload) {
const key = keyOf(payload);
const ledger = await loadLedger();
if (ledger[key]) {
return { id: ledger[key], created: false, reason: "ledger-hit", key };
}
const dup = await findBySlug(payload.slug);
if (dup) {
if (dup.meta?.ai_idempotency_key === key) {
ledger[key] = dup.id;
await writeFile(LEDGER, JSON.stringify(ledger, null, 2));
return { id: dup.id, created: false, reason: "same-key-exists", key };
}
throw new Error(`slug conflict: ${payload.slug} already belongs to post ${dup.id}`);
}
const post = await wp("/wp-json/wp/v2/posts", {
method: "POST",
body: JSON.stringify({
title: payload.title,
content: payload.content,
excerpt: payload.excerpt,
slug: payload.slug,
categories: [await resolveCategoryId(payload.category_slug)],
status: "draft", // 定数。payloadからは受け取らない
meta: {
ai_idempotency_key: key,
ai_source_run_id: payload.source_run_id,
},
}),
});
if (post.status !== "draft" || post.meta?.ai_idempotency_key !== key) {
throw new Error(`unexpected response for post ${post.id}`);
}
ledger[key] = post.id;
await writeFile(LEDGER, JSON.stringify(ledger, null, 2));
return { id: post.id, created: true, reason: "created", key };
}
読むべき箇所はstatus: "draft"の1行と、応答のpost.statusを再確認する部分です。さらに、認証ユーザーからpublish_postsを外すことで防御を重ねます。metaの2項目は、テーマまたはプラグイン側でregister_post_metaを使い、型をstring、singleとshow_in_restをtrueにして事前登録してください。未登録のkeyはREST APIで読み書きできません。最初の接続試験では、POST応答とGET応答の両方でkeyが保存されていることを確認します。
このコードは、同時に1件だけ処理するsingle-workerを前提にした最小例です。ローカル台帳は秘密情報ではありませんが、原稿の識別情報を含む監査データなのでアクセス制御し、backup対象にします。台帳が壊れている、読めない、JSONとして不正という状態を空の台帳として続行すると重複を招くため、ENOENT以外は停止させています。
重複抑止と再試行を実装する
WordPressの投稿作成endpointには、Idempotency-Keyヘッダーを受けて作成結果を再利用する仕組みがありません。下の3方式を重ねると単純な再実行には強くなりますが、照会とPOSTは原子的な1操作ではありません。そのため、ここで作るのはsingle-worker向けの重複抑止であり、厳密なidempotencyの保証ではありません。
| 方式 | 止められる重複 | 弱点 |
|---|---|---|
| ローカル台帳(key → post ID) | 同じジョブの単純な再実行。API呼び出し0回で判定できるので速い | 台帳ファイルを失うと機能しない。実行環境が複数あると共有できない |
| slugの事前照会 | 台帳を失った後の再実行と、既存記事とのslug衝突を検出する | AIがslugを変えると別物になる。照会と登録の間に競合の隙間がある |
| post metaへのkey保存 | 同じslugが同じ生成job由来かを区別し、事後の突き合わせに使う | 事前登録が必要。coreの一覧endpointでは任意meta検索を標準提供していない |
再試行の設計も重要です。タイムアウトは「サーバー側で未作成」を意味しません。POSTのレスポンスが返らなくても、投稿が作られている可能性があります。無条件に再送せず、次の順序で処理します。
- POSTがタイムアウトしたら、まず
findBySlugで作成済みかを確認する - 見つかった投稿の
ai_idempotency_keyが一致した場合だけ、台帳にIDを書いて正常終了する。異なる場合はslug衝突として人へ渡す - 見つからなかったときだけ、指数バックオフ(待ち時間を倍々に伸ばす方式)で再送する
- 401・403やschema違反を示す400は、認証・権限・payloadを直すまで再送しない。409や429などは応答の意味と
Retry-Afterを確認する - 一時的と判断できる5xxやネットワークエラーだけを、上限回数とjitter付き指数バックオフで再送する
台帳への書き込みはPOST成功の直後に行います。書き込み前に停止しても、次回はslugとpost metaのkeyを照合できます。ただし、複数のprocessや複数hostが同時に動くと、どちらも事前照会を通過してPOSTできます。この構成ではworkerを1つに固定してください。並列化する場合は、unique制約を持つ共有job store、分散lock、またはWordPress側のcustom endpointで「key確保と投稿作成」を排他的に扱う設計が必要です。
人間承認と監査ログをつなぐ
確認者・修正差分・承認日時を残す
下書きが積まれた後、誰がどう直して公開したのかを残していないと、問題が起きたときに経緯を再現できません。次の項目を1レコードとして記録します。post IDを軸にすれば、WordPress側の投稿と1対1で対応します。
| 記録する項目 | 値の例 | これがないと困ること |
|---|---|---|
| source_run_id | run-2026-07-28-014 | どの生成ジョブ由来か分からず、原因のプロンプトに辿り着けない |
| idempotency_key | a3f9…(32文字) | 重複登録が起きたときに、同一原稿かどうか判定できない |
| model / prompt_version | model-x / v7 | 品質が落ちたとき、モデル変更と手順変更のどちらが原因か切り分けられない |
| sources(URLと確認日) | https://example.com/spec / 2026-07-28 | 記載内容の根拠を後から示せない |
| validation_result | pass / fail: excerpt too short | どのルールで何件落ちているかが集計できず、schemaを改善できない |
| post_id / 登録時status | 1284 / draft | 登録された実体と記録が結び付かない |
| reviewer | 編集担当の識別子 | 公開物の責任の所在が消える。ここが監査ログの中心 |
| reviewed_at / decision | 2026-07-28T14:20+09:00 / approve | いつ・どの判断で公開へ進んだかを説明できない |
| diff_summary | 変更18行/差分ファイルへのパス | AI原稿がどれだけ手直しを要したかが測れず、改善点が見えない |
| published_at / published_by | 2026-07-28T15:02+09:00 / 編集担当 | 公開操作が人によるものだったことを示せない |
diff_summary(修正差分)は地味ですが、いちばん改善に効きます。毎回同じ箇所を人が直しているなら、それはプロンプトかschemaで解決すべき問題です。差分の行数だけでも記録しておけば、手順を変えた前後で必要な手直しが減ったかを数字で言えます。decisionは approve・revise・reject の3値に固定し、rejectの場合は理由を必須にしてください。
エラー時に公開せずqueueへ戻す
異常時の既定動作は「公開せず止める」にします。失敗時に処理を通すfail-openではなく、理由付きで保留し、人が再開条件を確認します。
- schema検証で落ちた:WordPressへは送らず、payloadと失敗理由をqueueへ戻す。部分的に直して送る経路は作らない
- categoryが解決できない:categoryを自動作成せず、未解決として保留する。人がcategoryを整備してから再実行する
- 認証エラー(401・403):即座にジョブ全体を停止する。鍵の失効や権限変更が起きているため、後続を流しても同じ失敗が続く
- 登録は成功したが後続処理で失敗:下書きは
draftのまま残す。中途半端な状態を消しに行かず、post IDをログに残して人が判断する - 再試行の上限に達した:そのpayloadを保留キューへ移し、次回実行の対象から外す。無限に再送させない
文字列検索だけでは、変数、hook、別endpointによる公開経路まで証明できません。自動テストでは、AIのschemaがstatusを拒否すること、POST bodyが常にdraftであること、応答statusを確認していること、認証ユーザーがpublish_postsを持たないことを検査します。WordPress側のpluginやhookでstatusが変更されていないかも、ステージングの統合テストで確かめます。
よくある失敗
AIにstatusを決めさせる
出力schemaに status を含め、「原則としてdraftにしてください」とプロンプトで指示する作りは、いずれ破れます。プロンプトの改訂、モデルの差し替え、few-shot例の混入のいずれでも出力は変わり、その瞬間に未確認の記事が公開されます。指示ではなくschemaとコードで塞いでください。
管理者アカウントの鍵をそのまま使う
管理者のApplication Passwordが漏れると、そのユーザーに許された広い操作へ悪用される可能性があります。publish_postsを持たない専用ユーザーを用意し、連携ごとに別のApplication Passwordを発行します。secretはリポジトリやログに置かず、不要時や漏えいの疑いがあるときは失効・再発行します。
タイムアウトを失敗とみなして再送する
レスポンスが返らなくても投稿は作られていることがあります。確認せずに再送すると、WordPressがslugを一意化し、2本目を作る場合があります。再送前にslugとidempotency keyを照合します。それでも同時実行の競合は防げないため、最小構成はsingle-workerに限定します。
よくある質問
Application PasswordsはHTTPSがないと使えませんか?
既定では、HTTPSで処理されるリクエストで利用できます。利用可否をfilterで変更できても、HTTPのBasic認証は資格情報を盗み見られる危険があります。本番ではHTTPSを必須にし、WordPressとreverse proxyのHTTPS判定も正しく設定してください。
登録した下書きが一覧APIで見つかりません
投稿一覧の status パラメータは既定値が publish です。status=draft を明示し、あわせて context=edit を付けてください。非公開の投稿を読むには、その投稿を編集できる権限での認証も必要です。
categoryはIDでなくslugで渡せませんか?
投稿作成の categories はtermのIDの配列を受け取ります。AIにはslugを出力させ、登録の直前に GET /wp-json/wp/v2/categories?slug=… でIDへ解決する形にしてください。解決結果が1件でなければ登録を止めます。存在しないcategoryを自動作成する実装は避けます。
予約投稿(future)なら自動化してよいですか?
推奨しません。future は指定日時に自動で公開される状態なので、人が読まないまま公開されるという点では publish と同じです。予約が必要な場合も、人が承認したうえで日時を設定する操作として扱ってください。
まとめ
AI下書きのWordPress自動化は、生成・検証・登録・承認を分け、自動化の終点を下書き登録に置きます。AI出力はJSON Schemaで受け、additionalProperties: falseとstatus field不在を組み合わせます。登録コードではstatus: "draft"をリテラルにし、応答でもdraftを確認します。さらに、Application Passwordをpublish_postsのない専用ユーザーへ紐づけ、コード以外の層でも公開を止めます。
single-workerの重複抑止には、ローカル台帳、slug照会、登録済みpost metaのkey照合を重ねます。これは同時POSTまで原子的に防ぐ仕組みではありません。複数workerで動かすなら、共有storeのunique制約やWordPress側の排他的なcustom endpointが必要です。タイムアウト後は未作成と決めつけず、再送前にslugとkeyを照合してください。
確認者、修正差分、承認日時、公開実行者を記録すると、問題発生時に制作過程を追跡できます。手直しの量と理由を集計すれば、prompt、schema、sanitizer、編集基準のどこを直すべきかも判断しやすくなります。登録する原稿そのものをどう書かせるかについては「AIブログ記事の書き方」を参照してください。
公式情報
- WordPress REST API Handbook:Posts(投稿の作成・取得・更新)
- WordPress Code Reference:WP_REST_Posts_Controller::create_item()
- WordPress REST API Handbook:Authentication(認証)
- WordPress REST API Handbook:Global Parameters
- WordPress REST API Handbook:Categories
- WordPress Advanced Administration Handbook:Application Passwords
- WordPress REST API Handbook:Modifying Responses(REST APIでのmeta登録)
- WordPress Documentation:Roles and Capabilities
- JSON Schema Draft 2020-12:Validation
- Node.js:Release schedule
- Google 検索セントラル:役立つ、信頼できる、ユーザー第一のコンテンツの作成