PR
私が使っているPCガジェット類
作業環境で実際に使っている・気になっている周辺機器などをまとめました
※ 一部のリンクは広告(アフィリエイト)を含みます
作業環境で実際に使っている・気になっている周辺機器などをまとめました
※ 一部のリンクは広告(アフィリエイト)を含みます
「AIに記事を書かせて、そのままWordPressに下書き保存できたら楽なのに」と思ったことはないですか?
今回は Claude Code で WordPress に自動投稿する仕組みを MCP(AIに外部サービスを操作させる仕組み)で作りました
投稿先はXserverで動かしている自分のブログで、届くのは下書きの状態
そのMCPサーバー自体、私がPythonを一行ずつ書いたというよりClaude Code本人に「こういうサーバーを作って」と頼んで作ってもらったんです
なので中心になるのは、Claudeにどう頼めばMCPサーバーができあがるのか、どうテストすれば任せられる状態になるのか、のあたり
すぐ動かしたい人向けに、いま動かしている全文も記事の末尾に折りたたんであります
セットアップが終わると、「VLOOKUPの解説記事を書いて、下書きで保存して」の一言でWordPress管理画面の「投稿一覧」に下書きができあがります
「VLOOKUP関数について、初心者向けに1000字くらいで解説記事を書いて、下書きで保存しておいて」
下書きを作るだけでなく、装飾やメタの設定まで会話で済みます
もちろん公開前のプレビュー確認は管理画面で行いますが、それ以外の編集作業をブラウザでぽちぽち進めなくて済むのは思ったより楽なんです
ジャベ雄私は下書き作成を、ぜんぶこれに寄せました
1つだけコツがあって、「SWELLのステップブロックで書いて」と頼むだけではClaudeはあなたのテーマの正確なブロックの形までは分かりません
理由は単純で、ブロックの中身(HTML)がテーマごとに違うから
そこで有効なのが、使いたいブロックを並べた見本用の固定ページを1枚だけ作っておく方法です
その見本ページをClaudeに読み取らせると、実際のブロックのHTMLを覚えて、同じ形を記事の中で再現できるようになります
私の場合は、ステップ・FAQ・吹き出し・各種ボックスなどを1ページにまとめた見本を作って、それをClaudeに読ませてから装飾してもらう、という流れにしています
いちばんはっきりしているのが画像で、AIは記事の見た目を見ているわけではないし、必要なスクショを自分で撮ることもできません
「この記事に合うスクショを貼って」と頼んでも、返ってくるのは場所の提案まで
ローカルの画像をアップロードするにしても、メディアライブラリから選ぶにしても、「どれが適切か」の判断はAIには難しいと思います
ここは人の出番
まるごと任せられないだけで、下ごしらえは頼めます
「ここにスクショを入れる」という場所と、「何を写したスクショにするか」のメモを、該当箇所に書いておいてもらう使い方です
そうすれば人は、メモ通りにスクショを撮って貼るだけ
実際この記事の画像も、先にAIが「ここにアプリケーションパスワード発行画面のスクショ」とメモを置いておいて、その場所に私が撮ったものを入れています
このMCPはWordPressのREST API(記事・固定ページ・メディア・カテゴリなどを操作する窓口)を通して動いているので、そこに用意されていないこと——サイト全体に適用されるCSSやJavaScript、プラグインの設定、メニューやサイト構成の変更などは反映できません
記事1本ごとのCSSのほうは、後で触れる補助プラグインを入れれば設定できる範囲
JSも同じように開放はできますが、安全を考えるとおすすめしません
「こう変えたい」と相談すれば書き方や手順の提案はしてくれますが、実際に反映するのはテーマ・カスタマイザー・プラグイン側の作業で、MCPの守備範囲の外になります
逆に、REST API経由で触れるメタ情報(SEOのディスクリプションなど)は、できる中でもあえて最小限に絞っています
SEOのタイトルやog:imageなども、開放すればMCPから設定できる項目
ただ自動で書き換えられる範囲が広いほど、うっかり上書きや、認証情報が漏れたときの被害も大きくなります
なので「実際に自動化したいものだけ開けておく」のが安全だと思います
具体的には、SEOプラグインのメタディスクリプションやSWELLの投稿別CSSは、標準のWordPressでは外部から書き込めません
そこを開けるための補助プラグインをClaudeに作ってもらって、mu-pluginsという特別なフォルダに置いています(置き場所と中身は記事の末尾に)
この「Claudeに作ってもらって自分で置く」流れは、MCPサーバー本体を作るときと同じ
補助プラグインは1ファイルの短いPHPなので、依頼すればすぐ作ってもらえます
それから、これは「できない」というより最後は人が見ておきたいところですが、仕上がりの見た目
AIはブロックのHTMLを組むところまでで、テーブルの幅やモバイルでの崩れといった実際の表示までは見えていません
ファクトの裏取りと同じ感覚で、公開前にプレビューでさっと目を通しておくと取りこぼしが減ると思います
もう1つ、これはMCPというよりAIに記事を書かせるとき全般のコツですが、「リストには下線を付ける」「画像は影付きスタイルにする」のような自分ルールを決めておくと、AIはそれに合わせて作ってくれます
「MCP」はModel Context Protocolの略で、Anthropic(Claudeを作っている会社)が提唱しているAIアシスタント業界の新しい標準仕様です
ざっくり言うと、AIに外部のサービスを操作させるための共通言語のようなものになります
これまでもAIに「○○をやって」と頼むことはできましたが、AI自身は実際にWordPressを操作したりファイルを読み書きしたりはできませんでした
MCPサーバーを間に挟むことで、AIから外部システムを「ツール」として呼び出せるようになり、AIが実作業まで進められるようになります
図にするとこんな流れになります
[あなた] ⇄ [Claude Code] ⇄ [MCPサーバー] ⇄ [WordPress REST API] ⇄ [WordPress]
2026年9月に確認した時点では、WordPress公式のMCP Adapterはあるものの、投稿を作る・直す能力までは入っていません(くわしくは後ろの節で)
なので今回はPythonで自作しました
といっても最初はだいたい200行の小さなものをClaudeに作ってもらって、そこから少しずつツールを足していった形です
Claude Code自体の基本的な使い方はまた別記事で書こうと思いますが、その前段としてClaudeの使い方を初心者向けに解説した記事もあるので、Claudeに触ったことがない人はそちらから読んでもらえると入りやすいです
MCPサーバーは自分でゼロからコードを書かなくても、Claude Code本人に作ってもらえます
コツは一度に全部を作らせないことで、1つのツールを作る → 動かしてテスト → 次のツールを足すという小さいサイクルで育てていくと失敗しにくいです



最初に全部まとめて作らせて、見事にやらかしたんです
まずはこんなふうに、使う技術と認証の方針を伝えて土台を作ってもらいます
PythonでWordPressを操作するMCPサーバーを作りたいです
・FastMCP(mcp.server.fastmcp の FastMCP)を使う
・WordPress REST API(/wp-json/wp/v2/)を叩く
・認証はアプリケーションパスワードで、認証情報は .env から読む(python-dotenv)
・まずは接続確認のツール(サイト名とログインユーザーを返す)だけ作って
ここで認証情報は .env から読むと最初に指定しておくと、あとの作業が楽になります
コードの中にパスワードが残らないので、そのまま公開しても問題ない形
接続確認が通ったら、記事を作るツールから順番に足していきます
接続確認が通ったので、記事を下書き保存するツール create_post を追加して
デフォルトのstatusはdraft、publishは明示したときだけにして
同じ調子でlist_posts(一覧取得)、update_post(更新)、get_post(取得)と1つずつ頼んでいきます
カテゴリやタグの一覧取得・新規作成も、同じやり方
Python・Claude Code(Pro以上)・アプリケーションパスワードの3つが揃えば、6ステップで接続確認まで終わります
| 必要なもの | 補足 |
|---|---|
| Claude Code(Desktop版) | ClaudeのPro / Maxプランで利用可能 |
| WordPress(自己ホスト型) | Xserver、ロリポップ、ConoHaなど、バージョンは5.6以上 |
| Python 3.10以上 | Windows 11なら公式インストーラーで導入 |
| PowerShell | Windows 11標準搭載、コマンドを数行打つだけ |
Claude Codeは有料プラン専用なので、無料で試したい場合はClaudeのコネクタ機能を使う代替手段もあります
任意の場所にフォルダを作ります
私の場合はデスクトップ
C:\Users\ユーザー名\Desktop\blog\前の章で作ってもらったwp_mcp_server.pyに加えて、動かすために次の3ファイルが必要です
いずれもClaudeに頼めば、そのまま作って配置までやってくれます
こんな感じで頼めばまとめて作ってくれます
このサーバーを動かすのに必要な requirements.txt と .mcp.json も作って、同じフォルダに置いて
.env は中身が認証情報なので、空のテンプレート(WP_BLOG_BASE_URL / WP_BLOG_USERNAME / WP_BLOG_APP_PASSWORD の3つを書く枠)だけ作っておいて
.mcp.json や CLAUDE.md のような設定ファイルがどこに置かれて何をするのかは、最初はちょっと分かりにくいので、別記事で一覧にしてあります


.envの中身(=認証情報)だけは人が書きます
ここはClaudeに渡さない情報なので、空のテンプレートだけ作ってもらって、値は次のステップで自分で埋めるのが安全です
requirements.txtができたら、ライブラリのインストールもClaudeに頼めます
requirements.txt のライブラリをインストールして
これで mcp・requests・markdown などのパッケージが入ります
自分で実行したい場合は、PowerShellで次のコマンドでもOK
cd C:\Users\ユーザー名\Desktop\blog\
python -m pip install -r requirements.txtここが今回の肝
普段のログインパスワードではなく API専用の使い捨てパスワードを発行します




この画面を閉じると二度と表示されません
すぐ次のステップで貼り付けてください
Step2でClaudeに枠だけ作ってもらった .env ファイルを開いて、次の3つの値を自分で埋めます
WP_BLOG_BASE_URL=https://あなたのドメイン
WP_BLOG_USERNAME=WordPressのログインユーザー名
WP_BLOG_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxxアプリケーションパスワードはスペース込みのままで大丈夫なので、わざわざ削る必要はありません
末尾に載せた完全版は、旧い名前の WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD も同じ意味で読みます
このフォルダでClaude Codeを起動すると、.mcp.json が自動で検出されてMCPサーバーの起動許可を求められます
「Allow」を押してください
その後Claude Codeに次のように頼みます
「WordPressの接続確認をして」
サイト名とログインユーザー名が返ってくれば接続成功
ここでやっとひと息つけます
AI鬼管理|Claude Code活用の業務自動化トレーニング
Claude Code を業務の自動化に使うためのトレーニングです
いきなり申し込む形ではなく、まずは無料の業務効率化診断(オンライン面談)から始められます
MCPサーバーのテストはClaudeに実際に実行させて、会話で結果を確認するのが一番早いです
ツールを1つ足すたびに次の流れを回しておくと、どこかが壊れた瞬間に気づけます
本格的なテストコード(pytestなど)を書くのは、仕様が固まってからで十分だと思います
「WordPressの接続確認をして」と頼んで、サイト名とログインユーザー名が返ってくればまず合格です
「テスト用の下書きを1件作って、投稿IDを教えて」と頼みます
WordPress管理画面の投稿一覧に下書きが増えていれば作成は成功
「下書き一覧を出して」「さっきの投稿の中身を見せて」と頼んで、取得系のツールがちゃんと動くかを見ます
「さっきのテスト投稿を削除して」で後片付けまでできれば、作成から削除まで一通り通ったことになります
.envのパスワードを1文字だけ変えて、401(認証失敗)が分かりやすく返るかも見ておきます
エラーのときに何が起きたか分かる作りになっていれば、ひと安心
確認できたらパスワードは元に戻します
この「会話で1ツールずつ叩く」テストの積み重ねが、結局いちばん速くて確実でした
セットアップが終わったら、基本はチャットで会話しながら書いてもらうだけです
「ExcelのVLOOKUPについて、初心者向けに1000字程度で解説記事を書いて、下書きで保存しておいて」
これだけで、Claudeが記事本文を生成してcreate_postツール経由でWordPressに下書きとして投稿してくれます
あとは管理画面でプレビューして、必要なら手直しして「公開」を押すだけです
便利な仕組みですが、AIに丸投げで運用する前にいくつか注意点があります
特にハルシネーション対策は、生成AIの裏取りのやり方のところでも書いた通り、AI全般で気をつけたいポイントです
2026年9月に確認した限りでは、プラグインを2つ入れるだけでも記事の下書き投稿までは届きます
ただし私の環境で公式ルートを丸ごと動かしてみたわけではないので、ここからは公開されている説明を読んだ範囲での整理
土台になるのがWordPress公式のMCP Adapterで、WordPress側にAIからの窓口を1つ開けるだけの部品みたいな感じです
でも、入れただけで見えるようになるのは discover / get-ability-info / execute の3つで、肝心の能力は既定で非公開のまま
WordPress 6.9本体が持っている能力も、サイト情報・ユーザー情報・環境情報の3つだけ
投稿を作る・直す能力は7.2以降に先送りと公式の提案に書かれていて、読み取り専用のものすら7.1では見送られました
だから、足りないところを外から足す橋渡しプラグインが出てきています(2つとも動かすには WordPress 6.9以上が前提)
Enable Abilities for MCPは投稿の作成・更新・削除を含む112個の能力を足してくれて、Claude.aiから繋ぐためのOAuthコネクタまで入っているとのことでした
MCPやコネクタといった言葉の意味は、Claude用語集のほうにまとめてあります
私は公式ルートに寄せるかでちょっと迷いましたが、結局この記事の自作方式のままです
SEO SIMPLE PACK のメタだったり、3サイトの切り替えだったりが要るので、いまの公式ルートだと足りないままなんです
逆にWordPressは1サイトだけ・SEOはYoastかRank Math・自作は避けたいという条件なら、やっぱり公式ルートのほうが近道だと思います



公式が育ったら、自作のほうは引退させる予定です
AIに渡せたのは、下書き作成・装飾・メタ設定の3つでした
| やったこと | できるようになったこと |
|---|---|
| MCPサーバー本体をClaudeに作ってもらう | Claude CodeからWordPressを直接操作(記事・カテゴリ・タグ・メタ・アイキャッチ) |
| アプリケーションパスワードで認証 | 普段のログインパスワードを使わずに済む |
| 補助プラグインで触れる範囲を選ぶ | 必要な項目(メタディスクリプション・投稿別CSS)だけ安全に開放 |
最初の接続確認さえ通れば「テーマを伝える → 下書きが管理画面にできてる」という流れがとても快適です
個人的に思うのは、「AIに全部任せる」じゃなくて、手間のかかる部分をAIに渡して、最後の判断は人間がする、この役割分担がちょうど良い距離感でした
ここまで読むと細かい手順や注意点が多く見えるかもしれませんが、実際にはこの記事で書いたほど細かく指示しなくても、Claudeが自分で判断してよしなに進めてくれる場面が多いです
必要なら途中で「ここはこうして」と相談すれば直してくれるので、まずは小さく頼んで動かしてみるところから始めてみてください
同じくWordPress×AIの自動投稿を試したい方の参考になれば嬉しいです



私も最初の1個は、接続確認だけでした
完全版は18個のツールで、だいたい900行
複数サイトの切り替え・Markdownファイルからの一発入稿・アイキャッチの設定・メディアのアップロードまで入っている形
mcp>=1.2.0
requests>=2.32.0
python-dotenv>=1.0.0
markdown>=3.6
python-frontmatter>=1.1.0{
"mcpServers": {
"wordpress": {
"command": "python",
"args": ["C:\\path\\to\\wp_mcp_server.py"],
"env": {}
}
}
}from mcp.server.fastmcp import FastMCP
import os, requests
from dotenv import load_dotenv
load_dotenv()
WP_BASE_URL = os.getenv("WP_BASE_URL", "").rstrip("/")
WP_USERNAME = os.getenv("WP_USERNAME", "")
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD", "")
API_BASE = f"{WP_BASE_URL}/wp-json/wp/v2"
mcp = FastMCP("wordpress")
@mcp.tool()
def create_post(title: str, content: str, status: str = "draft") -> dict:
"""新しい記事をWordPressに作成する(デフォルトは下書き)"""
resp = requests.post(
f"{API_BASE}/posts",
auth=(WP_USERNAME, WP_APP_PASSWORD),
json={"title": title, "content": content, "status": status},
timeout=30,
)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
mcp.run()この抜粋は旧い名前の WP_BASE_URL のほうを読む形なので、WP_BLOG_* で揃えるなら下のコード全文を使ってください
短いコードですが、仕組みのポイントはこのあたりです
完成版もこの形で、create_post・update_post・upload_media などのツールを登録しています
このサーバーをそっくり真似しなくても、「こういうツールが欲しい」とClaudeに伝えて1つずつ足していくやり方なら、自分の運用に合った形が作れます
.env に書くキーは WP_BLOG_BASE_URL / WP_BLOG_USERNAME / WP_BLOG_APP_PASSWORD の3つ
メタディスクリプションまで書き込みたいときは、次の補助プラグインもセットで要ります
"""WordPress MCP Server (multi-site).
WordPress REST API(`/wp-json/wp/v2/`)とアプリケーションパスワード認証を使い、
Claude Code などの MCP クライアントから 投稿(posts)・固定ページ(pages)・メディア・
カテゴリ / タグ を操作するための MCP サーバー。
複数サイト対応:
.env に `WP_<NAME>_BASE_URL` / `WP_<NAME>_USERNAME` / `WP_<NAME>_APP_PASSWORD` を置くと、
`<NAME>` を小文字にしたものがサイト名になる(例: WP_BLOG_* → site="blog")。
サイトはいくつでも登録でき、各ツールの `site` 引数で切り替える。
任意の環境変数:
WP_DEFAULT_SITE : site 省略時に使うサイト名(未設定なら "blog" があれば blog、無ければ最初に見つかったサイト)
WP_<NAME>_AUTHOR_ID : 新規作成する投稿・固定ページの著者 user_id(更新には影響しない)
WP_META_DESCRIPTION_KEY : frontmatter の meta_description を書き込む post meta キー
(既定 "ssp_meta_description" = SEO SIMPLE PACK。空文字にすると書き込まない)
WP_<NAME>_META_DESCRIPTION_KEY : 上をサイト別に上書きしたい場合
※ 書き込み先の meta キーは REST 公開(register_post_meta の show_in_rest)されている必要がある。
SEO SIMPLE PACK 用の補助プラグインは mu-plugins/ssp-meta-rest.php を参照。
後方互換: 旧キー WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD は site="blog" の別名として認識する。
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
from typing import Any
import frontmatter
import markdown as md
import requests
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
ROOT = Path(__file__).resolve().parent
load_dotenv(ROOT / ".env")
TIMEOUT = 30
mcp = FastMCP("wordpress")
# =========================================================================
# サイト別認証情報の解決
# =========================================================================
# サイトは環境変数から自動発見する: WP_<NAME>_BASE_URL の <NAME> を小文字化したものがサイト名
_SITE_KEY_RE = re.compile(r"^WP_([A-Z0-9]+)_BASE_URL$")
_LEGACY_BLOG_KEYS = {"BASE_URL": "WP_BASE_URL", "USERNAME": "WP_USERNAME", "APP_PASSWORD": "WP_APP_PASSWORD"}
def _discover_sites() -> list[str]:
"""登録済みサイト名を列挙する(blog を先頭に、あとは名前順)。"""
names: set[str] = set()
for key, val in os.environ.items():
m = _SITE_KEY_RE.match(key)
if m and val.strip():
names.add(m.group(1).lower())
if os.getenv("WP_BASE_URL", "").strip():
names.add("blog") # 旧キー(WP_BASE_URL / WP_USERNAME / WP_APP_PASSWORD)は blog の別名
return sorted(names, key=lambda n: (n != "blog", n))
def _env_for(site: str, suffix: str) -> str:
"""site 用の環境変数 WP_<SITE>_<suffix> を返す。site="blog" は旧キーにもフォールバックする。"""
val = os.getenv(f"WP_{site.upper()}_{suffix}", "").strip()
if not val and site == "blog" and suffix in _LEGACY_BLOG_KEYS:
val = os.getenv(_LEGACY_BLOG_KEYS[suffix], "").strip()
return val
def _default_site() -> str:
env = os.getenv("WP_DEFAULT_SITE", "").strip().lower()
if env:
return env
sites = _discover_sites()
if not sites or "blog" in sites:
return "blog"
return sites[0]
DEFAULT_SITE = _default_site()
def _default_author(site: str) -> int:
"""新規作成時に立てる著者(user_id)。WP_<SITE>_AUTHOR_ID が数値なら送る。
未設定なら author を送らない(= 認証ユーザーが著者)。更新系は author を送らないので影響しない。"""
raw = _env_for((site or DEFAULT_SITE).strip().lower(), "AUTHOR_ID")
return int(raw) if raw.isdigit() else 0
def _meta_description_key(site: str) -> str:
"""frontmatter の meta_description を書き込む post meta キー。空文字なら書き込まない。
優先順: WP_<SITE>_META_DESCRIPTION_KEY > WP_META_DESCRIPTION_KEY > "ssp_meta_description"(SEO SIMPLE PACK)。"""
site = (site or DEFAULT_SITE).strip().lower()
for key in (f"WP_{site.upper()}_META_DESCRIPTION_KEY", "WP_META_DESCRIPTION_KEY"):
if key in os.environ:
return os.environ[key].strip()
return "ssp_meta_description"
def _site_config(site: str) -> dict[str, str]:
site = (site or DEFAULT_SITE).strip().lower()
known = _discover_sites()
if site not in known:
raise ValueError(
f"未知の site 名: {site!r}。.env に登録済みのサイト: {known}"
f"(WP_{site.upper()}_BASE_URL / _USERNAME / _APP_PASSWORD を追加すると使えます)"
)
return {
"site": site,
"base_url": _env_for(site, "BASE_URL").rstrip("/"),
"username": _env_for(site, "USERNAME"),
"password": _env_for(site, "APP_PASSWORD"),
}
def _require_credentials(site: str) -> dict[str, str]:
cfg = _site_config(site)
missing = [
label
for label, val in (
("BASE_URL", cfg["base_url"]),
("USERNAME", cfg["username"]),
("APP_PASSWORD", cfg["password"]),
)
if not val
]
if missing:
prefix = f"WP_{cfg['site'].upper()}_"
env_hint = " / ".join(prefix + label for label in ("BASE_URL", "USERNAME", "APP_PASSWORD"))
raise RuntimeError(
f"site={cfg['site']!r} の認証情報が不足: {', '.join(missing)}。"
f"{ROOT / '.env'} に {env_hint} を設定してください"
)
return cfg
def _request(method: str, path: str, *, site: str = DEFAULT_SITE, **kwargs: Any) -> Any:
cfg = _require_credentials(site)
url = f"{cfg['base_url']}/wp-json/wp/v2{path}"
resp = requests.request(
method,
url,
auth=(cfg["username"], cfg["password"]),
timeout=TIMEOUT,
headers={"Accept": "application/json", **kwargs.pop("headers", {})},
**kwargs,
)
if not resp.ok:
raise RuntimeError(
f"WordPress API [{site}] {method} {path} failed: HTTP {resp.status_code} - {resp.text[:500]}"
)
if resp.status_code == 204 or not resp.content:
return None
return resp.json()
# =========================================================================
# 汎用ヘルパー
# =========================================================================
def _resolve_terms(taxonomy: str, names_or_ids: list[str | int], *, site: str) -> list[int]:
"""カテゴリ・タグの名前を ID に変換。存在しない名前は新規作成する。"""
if not names_or_ids:
return []
ids: list[int] = []
for item in names_or_ids:
if isinstance(item, int):
ids.append(item)
continue
s = str(item).strip()
if s.isdigit():
ids.append(int(s))
continue
existing = _request("GET", f"/{taxonomy}", site=site, params={"search": s, "per_page": 100})
match = next((t for t in existing if t.get("name") == s), None)
if match:
ids.append(int(match["id"]))
else:
created = _request("POST", f"/{taxonomy}", site=site, json={"name": s})
ids.append(int(created["id"]))
return ids
def _markdown_to_html(text: str) -> str:
return md.markdown(
text,
extensions=["extra", "sane_lists", "toc"],
output_format="html5",
)
def _entity_summary(entity: dict[str, Any]) -> dict[str, Any]:
"""投稿(post)または固定ページ(page)の共通サマリを返す。"""
return {
"id": entity.get("id"),
"status": entity.get("status"),
"title": (entity.get("title") or {}).get("rendered") or (entity.get("title") or {}).get("raw"),
"slug": entity.get("slug"),
"link": entity.get("link"),
"date": entity.get("date"),
"modified": entity.get("modified"),
"type": entity.get("type"),
"parent": entity.get("parent"),
"menu_order": entity.get("menu_order"),
"categories": entity.get("categories", []),
"tags": entity.get("tags", []),
}
# =========================================================================
# 接続確認
# =========================================================================
@mcp.tool()
def check_connection(site: str = DEFAULT_SITE) -> dict[str, Any]:
"""WordPress への接続と認証が正しいか確認する。
Args:
site: 接続先サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
サイト情報とログインユーザー名。エラー時は例外を投げる。
"""
cfg = _require_credentials(site)
site_resp = requests.get(
f"{cfg['base_url']}/wp-json",
auth=(cfg["username"], cfg["password"]),
timeout=TIMEOUT,
)
site_resp.raise_for_status()
site_info = site_resp.json()
me = _request("GET", "/users/me", site=site)
return {
"site": site,
"site_name": site_info.get("name"),
"site_url": site_info.get("url") or site_info.get("home"),
"logged_in_as": me.get("name"),
"user_id": me.get("id"),
"roles": me.get("roles"),
"available_sites": _discover_sites(),
}
# =========================================================================
# Posts (投稿)
# =========================================================================
@mcp.tool()
def create_post(
title: str,
content: str,
status: str = "draft",
excerpt: str = "",
slug: str = "",
categories: list[str | int] | None = None,
tags: list[str | int] | None = None,
date: str = "",
content_is_markdown: bool = False,
meta: dict[str, Any] | None = None,
featured_media: int = 0,
site: str = DEFAULT_SITE,
) -> dict[str, Any]:
"""新しい投稿を WordPress に作成する。
Args:
title: 投稿タイトル。
content: 本文。HTML を想定。Markdown を渡す場合は content_is_markdown=True にする。
status: "draft" (下書き / デフォルト), "publish" (公開), "future" (予約投稿), "pending", "private" のいずれか。
excerpt: 抜粋(任意)。
slug: URL スラッグ(任意)。
categories: カテゴリ名または ID の配列。名前が存在しなければ新規作成する。
tags: タグ名または ID の配列。名前が存在しなければ新規作成する。
date: 予約投稿用の日時 (ISO8601, 例 "2026-06-01T10:00:00")。status="future" 時に指定。
content_is_markdown: True なら content を Markdown として HTML に変換してから投稿する。
meta: post_meta に書き込むキー/値の辞書(例 {"ssp_meta_description": "..."})。キーは REST 公開されている必要あり。
featured_media: アイキャッチ画像のメディア ID(upload_media で取得した id)。0(デフォルト)なら設定しない。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
作成された投稿の概要。
"""
if content_is_markdown:
content = _markdown_to_html(content)
payload: dict[str, Any] = {"title": title, "content": content, "status": status}
if excerpt:
payload["excerpt"] = excerpt
if slug:
payload["slug"] = slug
if categories:
payload["categories"] = _resolve_terms("categories", categories, site=site)
if tags:
payload["tags"] = _resolve_terms("tags", tags, site=site)
if date:
payload["date"] = date
if meta:
payload["meta"] = meta
if featured_media:
payload["featured_media"] = featured_media
author_id = _default_author(site)
if author_id:
payload["author"] = author_id
post = _request("POST", "/posts", site=site, json=payload)
return _entity_summary(post)
@mcp.tool()
def update_post(
post_id: int,
title: str = "",
content: str = "",
status: str = "",
excerpt: str = "",
slug: str = "",
categories: list[str | int] | None = None,
tags: list[str | int] | None = None,
date: str = "",
content_is_markdown: bool = False,
meta: dict[str, Any] | None = None,
featured_media: int = 0,
site: str = DEFAULT_SITE,
) -> dict[str, Any]:
"""既存投稿を更新する。空文字を渡したフィールドは変更されない。
Args:
post_id: 更新対象の投稿 ID。
featured_media: アイキャッチ画像のメディア ID(upload_media で取得した id)。0(デフォルト)なら変更しない。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
その他: create_post と同じ。
Returns:
更新後の投稿の概要。
"""
payload: dict[str, Any] = {}
if title:
payload["title"] = title
if content:
payload["content"] = _markdown_to_html(content) if content_is_markdown else content
if status:
payload["status"] = status
if excerpt:
payload["excerpt"] = excerpt
if slug:
payload["slug"] = slug
if categories is not None:
payload["categories"] = _resolve_terms("categories", categories, site=site)
if tags is not None:
payload["tags"] = _resolve_terms("tags", tags, site=site)
if date:
payload["date"] = date
if meta is not None:
payload["meta"] = meta
if featured_media:
payload["featured_media"] = featured_media
if not payload:
raise ValueError("更新項目が指定されていません")
post = _request("POST", f"/posts/{post_id}", site=site, json=payload)
return _entity_summary(post)
@mcp.tool()
def get_post(post_id: int, context: str = "edit", site: str = DEFAULT_SITE) -> dict[str, Any]:
"""投稿 1 件を取得する。
Args:
post_id: 取得する投稿 ID。
context: "view" または "edit" (デフォルト)。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
投稿の全フィールド。
"""
return _request("GET", f"/posts/{post_id}", site=site, params={"context": context})
@mcp.tool()
def list_posts(
status: str = "any",
search: str = "",
per_page: int = 20,
page: int = 1,
category_ids: list[int] | None = None,
tag_ids: list[int] | None = None,
orderby: str = "date",
order: str = "desc",
site: str = DEFAULT_SITE,
) -> list[dict[str, Any]]:
"""投稿を一覧する。
Args:
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
他引数: 従来通り。
"""
params: dict[str, Any] = {
"status": status,
"per_page": min(max(per_page, 1), 100),
"page": page,
"orderby": orderby,
"order": order,
"context": "edit",
}
if search:
params["search"] = search
if category_ids:
params["categories"] = ",".join(str(i) for i in category_ids)
if tag_ids:
params["tags"] = ",".join(str(i) for i in tag_ids)
posts = _request("GET", "/posts", site=site, params=params)
return [_entity_summary(p) for p in posts]
@mcp.tool()
def delete_post(post_id: int, force: bool = False, site: str = DEFAULT_SITE) -> dict[str, Any]:
"""投稿を削除する。デフォルトはゴミ箱へ移動。
Args:
post_id: 削除する投稿 ID。
force: True なら完全削除、False (デフォルト) ならゴミ箱へ移動。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
"""
params = {"force": "true"} if force else {}
result = _request("DELETE", f"/posts/{post_id}", site=site, params=params)
return {"deleted": True, "force": force, "result": result}
# =========================================================================
# Pages (固定ページ) - /app/ 中心に使用
# =========================================================================
@mcp.tool()
def create_page(
title: str,
content: str,
status: str = "draft",
excerpt: str = "",
slug: str = "",
parent: int = 0,
menu_order: int = 0,
date: str = "",
content_is_markdown: bool = False,
meta: dict[str, Any] | None = None,
site: str = DEFAULT_SITE,
) -> dict[str, Any]:
"""新しい固定ページを WordPress に作成する。
Args:
title: ページタイトル。
content: 本文。HTML を想定。content_is_markdown=True なら Markdown を HTML 変換。
status: "draft" (下書き / デフォルト) / "publish" / "private" / "pending"。
excerpt: 抜粋(任意)。
slug: URL スラッグ(任意)。/app/{slug}/ になる。
parent: 親ページ ID。階層構造を作る場合に指定 (0 ならトップレベル)。
menu_order: 並び順(数字が小さいほど上)。
date: 公開日時 (ISO8601)。
content_is_markdown: True なら content を Markdown→HTML 変換。
meta: post_meta に書き込む辞書。独自メタキーは事前に register_post_meta(show_in_rest=true) で REST 公開されている必要あり。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
作成された固定ページの概要。
"""
if content_is_markdown:
content = _markdown_to_html(content)
payload: dict[str, Any] = {"title": title, "content": content, "status": status}
if excerpt:
payload["excerpt"] = excerpt
if slug:
payload["slug"] = slug
if parent:
payload["parent"] = int(parent)
if menu_order:
payload["menu_order"] = int(menu_order)
if date:
payload["date"] = date
if meta:
payload["meta"] = meta
author_id = _default_author(site)
if author_id:
payload["author"] = author_id
page = _request("POST", "/pages", site=site, json=payload)
return _entity_summary(page)
@mcp.tool()
def update_page(
page_id: int,
title: str = "",
content: str = "",
status: str = "",
excerpt: str = "",
slug: str = "",
parent: int | None = None,
menu_order: int | None = None,
date: str = "",
content_is_markdown: bool = False,
meta: dict[str, Any] | None = None,
site: str = DEFAULT_SITE,
) -> dict[str, Any]:
"""既存固定ページを更新する。空文字 / None を渡したフィールドは変更されない。
Args:
page_id: 更新対象の固定ページ ID。
parent: 親ページ ID を変更する場合に指定(明示的に 0 にすればトップレベル化)。
menu_order: 並び順を変更する場合に指定。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
その他: create_page と同じ。
Returns:
更新後の固定ページの概要。
"""
payload: dict[str, Any] = {}
if title:
payload["title"] = title
if content:
payload["content"] = _markdown_to_html(content) if content_is_markdown else content
if status:
payload["status"] = status
if excerpt:
payload["excerpt"] = excerpt
if slug:
payload["slug"] = slug
if parent is not None:
payload["parent"] = int(parent)
if menu_order is not None:
payload["menu_order"] = int(menu_order)
if date:
payload["date"] = date
if meta is not None:
payload["meta"] = meta
if not payload:
raise ValueError("更新項目が指定されていません")
page = _request("POST", f"/pages/{page_id}", site=site, json=payload)
return _entity_summary(page)
@mcp.tool()
def get_page(page_id: int, context: str = "edit", site: str = DEFAULT_SITE) -> dict[str, Any]:
"""固定ページ 1 件を取得する。
Args:
page_id: 取得する固定ページ ID。
context: "view" (公開ビュー) または "edit" (デフォルト、編集用の raw 値を含む)。
site: 取得先サイト。
Returns:
固定ページの全フィールド。
"""
return _request("GET", f"/pages/{page_id}", site=site, params={"context": context})
@mcp.tool()
def list_pages(
status: str = "any",
search: str = "",
parent: int | None = None,
per_page: int = 50,
page: int = 1,
orderby: str = "menu_order",
order: str = "asc",
site: str = DEFAULT_SITE,
) -> list[dict[str, Any]]:
"""固定ページを一覧する。
Args:
status: "any", "publish", "draft", "future", "pending", "private"。
search: タイトル/本文の部分検索キーワード。
parent: 親ページ ID で絞り込み(0 を指定するとトップレベルのみ取得)。
per_page: 1 ページあたりの件数 (最大 100)。
page: ページ番号。
orderby: "menu_order" (デフォルト・並び順)、"date"、"modified"、"title"、"id"。
order: "asc" (デフォルト) または "desc"。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
固定ページ概要の配列(id / slug / title / link / parent / menu_order など)。
"""
params: dict[str, Any] = {
"status": status,
"per_page": min(max(per_page, 1), 100),
"page": page,
"orderby": orderby,
"order": order,
"context": "edit",
}
if search:
params["search"] = search
if parent is not None:
params["parent"] = parent
pages = _request("GET", "/pages", site=site, params=params)
return [_entity_summary(p) for p in pages]
@mcp.tool()
def delete_page(page_id: int, force: bool = False, site: str = DEFAULT_SITE) -> dict[str, Any]:
"""固定ページを削除する。デフォルトはゴミ箱へ移動。
Args:
page_id: 削除する固定ページ ID。
force: True なら完全削除、False ならゴミ箱へ。
site: 対象サイト。
"""
params = {"force": "true"} if force else {}
result = _request("DELETE", f"/pages/{page_id}", site=site, params=params)
return {"deleted": True, "force": force, "result": result}
# =========================================================================
# Categories / Tags
# =========================================================================
@mcp.tool()
def list_categories(per_page: int = 100, search: str = "", site: str = DEFAULT_SITE) -> list[dict[str, Any]]:
"""カテゴリを一覧する。"""
params: dict[str, Any] = {"per_page": min(max(per_page, 1), 100)}
if search:
params["search"] = search
cats = _request("GET", "/categories", site=site, params=params)
return [
{"id": c["id"], "name": c["name"], "slug": c["slug"], "count": c.get("count", 0), "parent": c.get("parent", 0)}
for c in cats
]
@mcp.tool()
def create_category(
name: str, slug: str = "", description: str = "", parent: int = 0, site: str = DEFAULT_SITE
) -> dict[str, Any]:
"""カテゴリを新規作成する。"""
payload: dict[str, Any] = {"name": name}
if slug:
payload["slug"] = slug
if description:
payload["description"] = description
if parent:
payload["parent"] = parent
cat = _request("POST", "/categories", site=site, json=payload)
return {"id": cat["id"], "name": cat["name"], "slug": cat["slug"]}
@mcp.tool()
def list_tags(per_page: int = 100, search: str = "", site: str = DEFAULT_SITE) -> list[dict[str, Any]]:
"""タグを一覧する。"""
params: dict[str, Any] = {"per_page": min(max(per_page, 1), 100)}
if search:
params["search"] = search
tags = _request("GET", "/tags", site=site, params=params)
return [
{"id": t["id"], "name": t["name"], "slug": t["slug"], "count": t.get("count", 0)}
for t in tags
]
@mcp.tool()
def create_tag(name: str, slug: str = "", description: str = "", site: str = DEFAULT_SITE) -> dict[str, Any]:
"""タグを新規作成する。"""
payload: dict[str, Any] = {"name": name}
if slug:
payload["slug"] = slug
if description:
payload["description"] = description
tag = _request("POST", "/tags", site=site, json=payload)
return {"id": tag["id"], "name": tag["name"], "slug": tag["slug"]}
# =========================================================================
# Markdown ファイル経由(投稿/固定ページ両対応)
# =========================================================================
@mcp.tool()
def post_from_markdown_file(
file_path: str, override_status: str = "", override_site: str = ""
) -> dict[str, Any]:
"""Markdown ファイル(frontmatter 付き)から投稿または固定ページを作成または更新する。
frontmatter の `id` フィールドがあれば update、なければ create。
create 後は元ファイルの frontmatter に `id` を書き戻して、次回以降が update になる。
frontmatter 共通キー:
title (str, 必須), status ("draft"/"publish"/...), excerpt, slug,
date (ISO8601), id (int, 更新用), format ("markdown"/"gutenberg"/"html"),
meta_description (str, post meta へ書き込む。キーは WP_META_DESCRIPTION_KEY で指定、既定 ssp_meta_description=SEO SIMPLE PACK),
meta (dict, 任意の post_meta を直接指定。キーは REST 公開されている必要あり),
site (str, 投稿先サイト名。.env の WP_<NAME>_* で登録した名前)
投稿(post)用キー(post_type 未指定または "post"):
categories (list), tags (list)
featured_media (int): アイキャッチ画像のメディア ID(upload_media で取得した id)。
指定すると create_post/update_post に渡り投稿に紐づく。未指定なら変更しない。
※ 固定ページ(page)側は create_page/update_page 未対応のため非対応。
固定ページ(page)用キー(post_type / type に "page" を指定):
parent (int): 親ページ ID
menu_order (int): 並び順
Args:
file_path: 対象 Markdown ファイルの絶対または相対パス。
override_status: 指定すると frontmatter の status を上書きする。
override_site: 指定すると frontmatter の site を上書きする。
Returns:
作成/更新された投稿または固定ページの概要。
"""
path = Path(file_path)
if not path.is_absolute():
path = ROOT / path
if not path.exists():
raise FileNotFoundError(f"ファイルが見つかりません: {path}")
doc = frontmatter.load(path)
fm = doc.metadata
body = doc.content
title = fm.get("title")
if not title:
raise ValueError("frontmatter に title が必要です")
status = override_status or fm.get("status") or "draft"
site = (override_site or fm.get("site") or DEFAULT_SITE).strip().lower()
post_type = (fm.get("post_type") or fm.get("type") or "post").strip().lower()
fmt = (fm.get("format") or "markdown").lower()
html = body if fmt in ("html", "gutenberg", "raw") else _markdown_to_html(body)
post_meta: dict[str, Any] = {}
# meta_description は _meta_description_key(site) のキーに書き込む
# (既定は SEO SIMPLE PACK の ssp_meta_description、環境変数 WP_META_DESCRIPTION_KEY で変更可・空文字で無効化)
# ※ 書き込み先キーは REST 公開されている必要がある(同梱の mu-plugins/ssp-meta-rest.php 参照)
meta_key = _meta_description_key(site)
if fm.get("meta_description") and meta_key:
post_meta[meta_key] = fm["meta_description"]
# 任意の post_meta を直接指定したい場合は frontmatter の meta(dict)を使う
if isinstance(fm.get("meta"), dict):
post_meta.update(fm["meta"])
import datetime as _dt
date_val = fm.get("date") or ""
if isinstance(date_val, (_dt.datetime, _dt.date)):
date_val = date_val.isoformat()
elif date_val and not isinstance(date_val, str):
date_val = str(date_val)
record_id = fm.get("id")
excerpt_val = fm.get("excerpt", "") or ""
slug_val = fm.get("slug", "") or ""
featured_media_val = int(fm["featured_media"]) if fm.get("featured_media") else 0
if post_type == "page":
if record_id:
result = update_page(
page_id=int(record_id),
title=title,
content=html,
status=status,
excerpt=excerpt_val,
slug=slug_val,
parent=int(fm["parent"]) if fm.get("parent") is not None else None,
menu_order=int(fm["menu_order"]) if fm.get("menu_order") is not None else None,
date=date_val,
meta=post_meta or None,
site=site,
)
else:
result = create_page(
title=title,
content=html,
status=status,
excerpt=excerpt_val,
slug=slug_val,
parent=int(fm.get("parent") or 0),
menu_order=int(fm.get("menu_order") or 0),
date=date_val,
meta=post_meta or None,
site=site,
)
doc.metadata["id"] = result["id"]
path.write_text(frontmatter.dumps(doc), encoding="utf-8")
else:
if record_id:
result = update_post(
post_id=int(record_id),
title=title,
content=html,
status=status,
excerpt=excerpt_val,
slug=slug_val,
categories=fm.get("categories"),
tags=fm.get("tags"),
date=date_val,
meta=post_meta or None,
featured_media=featured_media_val,
site=site,
)
else:
result = create_post(
title=title,
content=html,
status=status,
excerpt=excerpt_val,
slug=slug_val,
categories=fm.get("categories"),
tags=fm.get("tags"),
date=date_val,
meta=post_meta or None,
featured_media=featured_media_val,
site=site,
)
doc.metadata["id"] = result["id"]
path.write_text(frontmatter.dumps(doc), encoding="utf-8")
return result
@mcp.tool()
def markdown_to_html(text: str) -> str:
"""Markdown を WordPress 投稿向けの HTML に変換する(プレビュー用)。"""
return _markdown_to_html(text)
# =========================================================================
# メディア(画像)アップロード
# =========================================================================
@mcp.tool()
def upload_media(
file_path: str,
filename: str = "",
title: str = "",
alt_text: str = "",
site: str = DEFAULT_SITE,
) -> dict[str, Any]:
"""ローカルの画像ファイルを WordPress のメディアライブラリにアップロードする。
Args:
file_path: アップロードする画像の絶対/相対パス。
filename: メディアのファイル名(省略時は元ファイル名)。★Content-Disposition は ASCII 推奨なので、
日本語ファイル名のときは英数字+ハイフンの名前(例 "screenshot-01.png")を明示指定する。
title: メディアのタイトル(任意)。
alt_text: 代替テキスト(任意、SEO/アクセシビリティ用)。
site: 対象サイト名(.env の WP_<NAME>_* で登録した名前)。省略時は既定サイト。
Returns:
{id, source_url, mime_type, title}。source_url を wp:image ブロックの src に使う。
"""
import mimetypes
cfg = _require_credentials(site)
path = Path(file_path)
if not path.is_absolute():
path = ROOT / path
if not path.exists():
raise FileNotFoundError(f"ファイルが見つかりません: {path}")
name = filename or path.name
ctype = mimetypes.guess_type(name)[0] or mimetypes.guess_type(str(path))[0] or "application/octet-stream"
data = path.read_bytes()
resp = requests.post(
f"{cfg['base_url']}/wp-json/wp/v2/media",
auth=(cfg["username"], cfg["password"]),
headers={
"Content-Disposition": f'attachment; filename="{name}"',
"Content-Type": ctype,
},
data=data,
timeout=TIMEOUT * 3,
)
if not resp.ok:
raise RuntimeError(
f"media upload failed [{site}]: HTTP {resp.status_code} - {resp.text[:500]}"
)
m = resp.json()
if title or alt_text:
patch: dict[str, Any] = {}
if title:
patch["title"] = title
if alt_text:
patch["alt_text"] = alt_text
m = _request("POST", f"/media/{m['id']}", site=site, json=patch)
title_val = m.get("title")
if isinstance(title_val, dict):
title_val = title_val.get("rendered") or title_val.get("raw")
return {
"id": m.get("id"),
"source_url": m.get("source_url"),
"mime_type": m.get("mime_type"),
"title": title_val,
}
if __name__ == "__main__":
try:
mcp.run()
except Exception as e:
print(f"MCP server error: {e}", file=sys.stderr)
raise置き場所は /wp-content/mu-plugins/ssp-meta-rest.php、mu-plugins(must-use plugins)フォルダが無ければ wp-content/ の下に自分で作ります
ここに置くと管理画面に項目も出ないまま自動で有効になるので、有効化のクリックは不要
テーマやプラグインを更新しても消えないので、自作の小さな仕組みを常駐させるのにちょうど良い場所
JavaScriptを書き込む機能だけは、あえて公開していません
認証情報が漏れたときに、好きなスクリプトを埋め込まれかねないからです
<?php
/**
* Plugin Name: SEO SIMPLE PACK Meta REST API Support
* Description: SEO SIMPLE PACK のメタフィールド + SWELL の投稿別カスタムCSS を WP REST API 経由で読み書きできるようにする補助プラグイン。投稿(post)・固定ページ(page)の両方に対応(/app/ のアプリ庫サイトは固定ページ運用のため page 必須)。
* Author: Local helper
* Version: 1.2.0
*
* 配置場所: /wp-content/mu-plugins/ssp-meta-rest.php
* (mu-pluginsフォルダが無ければ作成すること。配置するだけで自動有効化される)
*
* 変更履歴:
* 1.0.0 - 初版(post のみ対応)
* 1.1.0 - page 型にも対応(/app/ サイト用)
* 1.2.0 - SWELL の投稿別カスタムCSS(swell_meta_css)を追加して MCP から記事ごとのCSSを書けるようにした。
* ※ JS(swell_meta_js)は「あえて公開しない」: 認証情報(アプリパスワード)が漏れた場合に
* サイト訪問者へ任意の JavaScript を注入できてしまう(実質 XSS)リスクがあるため。
* ※ そのまま出力フラグ(swell_meta_css_plane / swell_meta_js_plane)も、操作面・事故面を
* 増やさないため公開しない(CSS は既定=<style> で囲む、で十分)。
*
* メタキーの根拠:
* - SEO SIMPLE PACK の post meta: ssp_meta_description / ssp_meta_robots(先頭アンダースコアなし)。
* - SWELL の投稿別CSS: swell_meta_css(投稿編集画面のメタボックス textarea name="swell_meta_css" より確認)。
* いずれも標準では REST 非公開のため、本プラグインで show_in_rest 登録する。
* ※ 一括書き戻し前に1記事で読み書きを検証することを推奨。
*/
add_action('init', function () {
// REST 公開するメタキー(値はすべて文字列)
$rest_meta_keys = [
'ssp_meta_description', // SSP メタディスクリプション(書き戻しの本命)
'ssp_meta_robots', // SSP robots(noindex 等。必要な記事のみ)
'swell_meta_css', // SWELL 投稿別カスタムCSS(wp_head で出力)。JS はあえて非公開
];
// 対象 post type:post(blog 用)+ page(/app/ 用、SWELL 固定ページ運用)
$target_post_types = ['post', 'page'];
foreach ($rest_meta_keys as $key) {
foreach ($target_post_types as $post_type) {
register_post_meta($post_type, $key, [
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () use ($post_type) {
// post なら edit_posts、page なら edit_pages の権限チェック
return $post_type === 'page'
? current_user_can('edit_pages')
: current_user_can('edit_posts');
},
]);
}
}
});更新版は javeo2022/wordpress-mcp-server にも置いています
WORK
ツール作成のご依頼を受けています
クラウドワークス・ココナラで受注200件以上
会社員の副業なので、対応は平日夜と土日です
ココナラが初めての方は、紹介コード 78E62K で1,000円分のポイントがもらえます
VBAとPythonを中心にユーザー側でできるITを自己学習しているので備忘録半分、学習履歴を残して同じ道を辿る人の参考になればとブログを始めました
副業でスクレイピングツール作成を中心にできることを色々やっていますのでご相談いただけるとありがたいです!
クラウドワークスのページへ
ココナラのページへ
コメント