Claude CodeでWordPressを運用する
記事投稿・SEO一括更新の実践手順
このサイト posii.net は WordPress で動いていますが、記事の下書き作成、SEOメタの設定、アイキャッチ画像の登録といった日々の運用のほとんどを、いまは Claude Code に任せています。この記事では、その具体的なやり方と、実際にやらかした失敗、そこから決めたルールをまとめます。
「Claude Code で WordPress の記事を投稿できるの?」「WordPress の SEO をAIで一括更新したい」という方に向けて、実際に使っているコードをそのまま(小さくして)載せています。
前提:posii.net の構成
まず、うちの環境です。特別なものは使っていません。
- WordPress本体:テーマは Neve、エディタはクラシックエディタ。
- SEO:SEO SIMPLE PACK(以下 SSP)でタイトル・ディスクリプション・キーワードを管理。
- 自作の mu-plugin:JSON-LD やパンくず、SSPメタのREST公開などをまとめたもの。mu-plugin は有効化不要で、プラグイン更新でも消えません。
- 操作する側:Claude Code + ログイン済みのブラウザ(Chrome拡張 or アプリ内ブラウザ)。
ポイントは、AIにWordPressのパスワードを渡していないことです。ログインは人間がブラウザで済ませ、Claude Code はそのログイン済みタブの中で JavaScript を実行して、WordPress 標準の REST API を叩きます。アプリケーションパスワードも発行していません。
基本の仕組み:ログイン済みブラウザ+X-WP-Nonce
WordPress の REST API(/wp-json/wp/v2/…)は、ログイン中のブラウザからなら Cookie 認証で使えます。ただし書き込み系のリクエストには X-WP-Nonce ヘッダーが必要です。nonce は管理画面の中で取れます。
// wp-admin にログイン済みのタブで実行
const nonce = await (await fetch('/wp-admin/admin-ajax.php?action=rest-nonce',
{credentials: 'same-origin'})).text();
// 投稿編集画面(post.php)なら wpApiSettings.nonce にも入っている
ちなみに、投稿一覧(edit.php)には wpApiSettings が存在しません。最初はここで何度かつまずいたので、編集画面を開いてから取るか、上の rest-nonce を使うようにしています。
記事を下書きで作る:本文・タグ・SEOメタを1リクエストで
新規記事は、/wp-json/wp/v2/posts に POST します。大事なのは status: 'draft' を必ず明示することです。
const res = await fetch('/wp-json/wp/v2/posts', {
method: 'POST', credentials: 'same-origin',
headers: {'X-WP-Nonce': nonce, 'Content-Type': 'application/json'},
body: JSON.stringify({
title: '記事タイトル',
slug: 'english-slug',
status: 'draft',
content: STYLED_HTML, // ブランドCSS入りの本文
categories: [22], tags: [101, 102, 103],
excerpt: '一覧に表示する80〜120字の抜粋',
meta: {
ssp_meta_title: 'タイトル | posii',
ssp_meta_description: '検索結果に出す説明文',
ssp_meta_keyword: 'キーワード1,キーワード2'
}
})
});
const post = await res.json();
console.log(post.id, post.status); // → 1234 "draft"
本文の先頭には、サイトのデザイン(ネイビー×ゴールド)に合わせた <style> をまるごと入れています。.posii-article というクラスの中だけに効くCSSなので、テーマ側と干渉しません。
ここで1つ注意があります。WordPress の wpautop(改行を自動で <p> や <br> に変える機能)です。
- <style> は1行に圧縮する:CSSの中に改行があると
<br>が挿入されて壊れます。 - ブロックの間に空行を入れない:二重改行があると、そこに空の
<p>が差し込まれてレイアウトが崩れます。 - コードは <pre> に入れる:
<div>+white-space:preだと改行が<br>に変換され、行間が倍になります。<pre>の中は wpautop が触らないので安全です。
また、クラシックエディタの「ビジュアル」タブで <style> を入れると TinyMCE に消されます。REST で直接入れるか、「テキスト」タブに切り替えてから入れる必要があります。
記事は「本文・カテゴリ・タグ・SEOメタ・アイキャッチ・抜粋」の6点が揃って完成、と決めています。以前、本文とSEOメタだけ入れてアイキャッチと抜粋を忘れたことがあり、それ以来、保存後に context=edit で読み直して6点を確認するようにしました。
既存記事の部分修正:context=edit で raw を取る
公開済みの記事に1セクション足したい、という場面はよくあります。このとき、表示用の content.rendered ではなく、?context=edit を付けて保存されている元のHTML(content.raw)を取り、そこに文字列置換で追記します。
const id = 1234;
const j = await (await fetch(`/wp-json/wp/v2/posts/${id}?context=edit`,
{credentials: 'same-origin', headers: {'X-WP-Nonce': nonce}})).json();
let raw = j.content.raw;
const anchor = '<p class="pa-outro">';
// アンカーが本文中に1回だけ出てくることを確認してから置換
if (raw.split(anchor).length - 1 !== 1) throw new Error('アンカーが一意ではない');
raw = raw.replace(anchor, ADDED_SECTION + '\n' + anchor);
await fetch(`/wp-json/wp/v2/posts/${id}`, {
method: 'POST', credentials: 'same-origin',
headers: {'X-WP-Nonce': nonce, 'Content-Type': 'application/json'},
body: JSON.stringify({content: raw}) // status は送らない
});
アンカーが複数ヒットすると、意図しない場所に追記されます。出現回数が1であることを確認してから置き換えるのが、事故を防ぐいちばん簡単な方法です。
2026年9月24日には、この方法で既存の技術記事3本に新しいセクションを追記しました。タイトルとURLは一切変えず、既存の文章も書き換えず、追記部分だけを <!-- wp:html --> のHTMLブロックとして差し込む「足すだけ」の編集です。公開済み記事への更新は即座に本番へ反映されるので、既存部分に触らない形にしています(理由は後述の失敗談にあります)。
SEOメタを一括更新する:mu-plugin で show_in_rest
SSP のタイトル・ディスクリプション・キーワードは投稿メタ(postmeta)に保存されていますが、そのままでは REST API から読み書きできません。そこで、mu-plugin で3つのキーを show_in_rest 付きで登録しています。
<?php
// wp-content/mu-plugins/ に置く(有効化は不要)
add_action('init', function () {
foreach (['ssp_meta_title', 'ssp_meta_description', 'ssp_meta_keyword'] as $key) {
register_post_meta('post', $key, [
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () { return current_user_can('edit_posts'); },
]);
}
});
実際には、JSON-LD や llms.txt の生成と同じ mu-plugin の中の1関数にしていますが、最小構成にするとこれだけです。これで {meta: {ssp_meta_keyword: '…'}} を送れば、何十記事でもループで更新できます。
const updates = [
[101, {ssp_meta_description: '…', ssp_meta_keyword: '…'}],
[102, {ssp_meta_description: '…', ssp_meta_keyword: '…'}],
];
for (const [id, meta] of updates) {
const r = await fetch(`/wp-json/wp/v2/posts/${id}`, {
method: 'POST', credentials: 'same-origin',
headers: {'X-WP-Nonce': nonce, 'Content-Type': 'application/json'},
body: JSON.stringify({meta})
});
console.log(id, r.status);
}
posii.net ではこの方法で62記事のSSPメタを一括投入し、公開記事すべてに title / description / keywords / タグが入った状態にしました。PHP を置くときは、構文エラーでサイト全体が落ちないよう、先にlintしてから mu-plugins に入れています。また、mu-plugins にバックアップのコピーを置くと二重に読み込まれてエラーになるので、バックアップは別の場所に置きます。
アイキャッチ画像のアップロード:REST の media
画像は /wp-json/wp/v2/media に FormData で送ります。Content-Type ヘッダーは自分で付けません(ブラウザが multipart の境界付きで自動設定します)。
const fd = new FormData();
fd.append('file', blob, 'eyecatch.jpg'); // blob = 画像データ
fd.append('title', 'アイキャッチ');
fd.append('alt_text', '画像の説明文');
const m = await (await fetch('/wp-json/wp/v2/media', {
method: 'POST', credentials: 'same-origin',
headers: {'X-WP-Nonce': nonce}, body: fd
})).json();
// 記事に紐づける
await fetch('/wp-json/wp/v2/posts/1234', {
method: 'POST', credentials: 'same-origin',
headers: {'X-WP-Nonce': nonce, 'Content-Type': 'application/json'},
body: JSON.stringify({featured_media: m.id})
});
ローカルにある画像ファイルをブラウザのページに渡す部分が意外と厄介でした。ファイル選択ダイアログはAIから操作できず、base64 をスクリプトに直書きすると長さの関係で数文字欠けて画像が壊れました。いまは、ローカルで base64 化した文字列を OS のクリップボード経由でページに渡し、ページ側で Blob に戻す方法に落ち着いています。アップロード後は、返ってきたファイルサイズが元ファイルと一致するかを必ず確認します(0バイトで通ってしまったことがあるためです)。
確認は curl で本番を見る
「更新しました」という REST の返事だけでは信用しません。公開済みの記事は、本番のHTMLを取得して、SEOメタが実際に出力されているかを確認します。
curl -s https://posii.net/english-slug/ \ | grep -oE '<title>[^<]*</title>|<meta name="description" content="[^"]*"'
下書きは外から見えないので、?p=ID&preview=true のプレビューをブラウザで開いて目視します。コードブロックについては、.pa-code の中に <br> が紛れ込んでいないかも数えています。
安全のために決めているルール
AIに管理者権限のブラウザを触らせる以上、ルールは先に決めておく必要があります。うちでは次の4つを守っています。
- 作るのは下書きだけ:新規記事は必ず
status: 'draft'。下書きは外に出ないので、ここまでは確認なしで進めてよいことにしています。 - 公開は人間のGOが必要:公開ボタン、ステータス変更、予約投稿は、人間が明示的にOKを出すまで実行しません。
- パスワードを入力させない:ログインは人間が行い、AIはログイン済みのタブを使うだけです。
- 書き込む前に対象を確認する:編集画面で作業するときは、ページの
post_IDが狙った記事と一致するかを確かめてから書き込みます。画面遷移が終わる前に前の記事のページでスクリプトが走り、別の記事を書き換えかけたことがあるためです。
失敗から学んだこと
便利な反面、一括操作はミスも一括で起きます。実際にあったことを正直に書いておきます。
1. 一括操作で固定ページが消え、17日間404だった
2026年9月3日、古いテーマのデモページを片付ける一括削除の際に、公開中の固定ページ6件(会社概要、代表紹介、サービス紹介、お問い合わせの確認画面・完了画面など)が一緒にゴミ箱へ移動していました。約3秒で150件を超える削除が走っており、人の手の速さではありません。
問題は、17日間だれも気づかなかったことです。トップページのリンク切れで発見し、全件を復元しました。お問い合わせの確認・完了画面も消えていたので、その間は問い合わせの導線ごと止まっていました。
原因は、管理者権限なら公開中の固定ページが何の抵抗もなく消せる構造そのものにあります。そこで、固定ページのゴミ箱移動・削除を拒否するガード用の mu-plugin を設置しました。REST で会社概要やFAQページのゴミ箱移動を試し、エラーで拒否されることを確認しています。本当に消したいページだけ、カスタムフィールドで個別にロックを外す方式です。あわせて、毎日404を検知する見張りの定期タスクも動かしています。
もう1つおまけの教訓があります。復元作業中、お問い合わせページ本体がまだ非公開の状態で確認画面だけを先に戻したところ、WordPress が /contact/ へのアクセスを、似たスラッグの確認画面へ自動でリダイレクトしてしまいました。それ以来、リンクの点検は「200が返るか」ではなく「リダイレクトなしで200が返るか」で見ています。
2. 順位のある記事のタイトルを変えたら、約18位から約48位へ
2026年8月、SEOの一括作業のついでに、RAGについての記事のタイトルをAIに書き換えさせました。「より良いタイトル」のつもりでしたが、その週から平均順位が約18位から約48位に下がりました。
9月にタイトルを元に戻しましたが、SSPのディスクリプションは変更履歴が残らないので、元の文面は復元できませんでした。ディスクリプションは、実際に検索されている言葉に合わせて書き直しています。
いまは次のように決めています。
- 変える前に Search Console を見る:そのページの現在の順位と、主にどの検索語で表示されているかを確認します。すでに順位が付いている記事は、一括作業では変えません。変えるなら1本ずつ提案して、人間がOKしてからです。
- 旧い値を記録してから変える:元の title / SSPタイトル / ディスクリプションをメモに残し、いつでも戻せるようにします。
- 変えたら2〜4週間後に順位を見る:変更の影響を必ず確認します。
先ほど紹介した「既存記事3本への追記」でタイトルとURLを変えなかったのは、この経験があるからです。
3. wpautop に <style> と <pre> を壊された
最初のころは、CSSを複数行で書いてレイアウトが崩れたり、コードを <div> で囲んで行間が倍になったりしました。どちらも wpautop が改行を <br> や <p> に変えるのが原因です。前述のとおり、CSSは1行に圧縮し、コードは <pre> に入れることで解決しました。投稿後はプレビューで崩れていないかを確認するところまでを、作業の手順に含めています。
まとめ
Claude Code で WordPress を運用するといっても、特別な連携プラグインは使っていません。使っているのは、ログイン済みのブラウザ、WordPress 標準の REST API、そして SEO SIMPLE PACK のメタを REST に公開する数行の mu-plugin だけです。
それよりも大事なのは運用ルールのほうでした。「下書きまでは任せる、公開は人間が決める」「順位のある記事のタイトルは勝手に変えない」「消えて困るものはシステム側で消せなくする」。この3つを決めてから、安心して任せられる範囲がかなり広がりました。
よくある質問
Q. 専用のプラグインは必要ですか?
A. 記事の作成・編集・画像アップロードだけなら、WordPress 標準の REST API で足ります。SEO SIMPLE PACK のメタを REST から書き換えたい場合だけ、この記事で紹介した数行の mu-plugin(register_post_meta で show_in_rest を有効にするもの)が必要です。
Q. Gutenberg(ブロックエディタ)のサイトでも使えますか?
A. REST API の使い方は同じです。posii.net はクラシックエディタですが、ブロックエディタでも本文はブロックのコメント付きHTMLとして保存されているだけなので、context=edit で raw を取って編集する流れは変わりません。追記部分を HTMLブロック(wp:html)で囲めば、エディタで開いても崩れにくくなります。
Q. AIが勝手に記事を公開してしまうことはありませんか?
A. 新規記事は必ず status を draft にして作り、公開は人間が明示的にOKを出すまで実行しないルールにしています。ただし、公開済みの記事を REST で更新すると、その内容はすぐ本番に反映されます。既存記事への修正は、アンカーを確認した「足すだけ」の編集に限定しています。
Q. AIにWordPressのパスワードを教える必要はありますか?
A. ありません。ログインは人間がブラウザで行い、AIはそのログイン済みのタブの中でスクリプトを実行するだけです。REST API の認証は、ログイン中の Cookie と X-WP-Nonce で行われます。
Q. SEOの一括更新で順位が下がることはありますか?
A. あります。実際に、順位の付いていた記事のタイトルを変えて約18位から約48位に下がりました。変更前に Search Console で順位と検索語を確認し、旧い値を記録してから変える、という手順をおすすめします。
自社のWordPress運用をAIで回したい方は、Posiiにご相談ください。
WordPressの運用を、AIに任せられる形に。
記事の投稿やSEOの一括更新は、POSIIが自社サイトで実際に回している運用です。御社のサイトに合わせた自動化の組み込みや、社内で回せるようにする研修をご相談いただけます。
記事を書く時間を、書く中身を考える時間に。運用の手間はAIに、判断は人に残す形がおすすめです。