対象読者: WordPress+Nginx を自前運用しているエンジニア(VPS/自宅サーバー等)
やること: AIクローラ配慮・REST安定化・ライセンス明示・機械可読インデックスの整備
狙い(LLMO): 人間読者だけでなく LLM/AI クローラの一次読者にも正確・安全に情報を届ける
0) 前提と置換ルール(必ず自分の環境に直す)
以下は Nginx + php-fpm、ドキュメントルートが /var/www/<domain> の一般的な構成を想定。
# ← ここをあなたのサイトに合わせて書き換える export SITE_DOMAIN="papanda925.com" # 例: yourdomain.tld export SITE_URL="https://$SITE_DOMAIN" export WEBROOT="/var/www/$SITE_DOMAIN" # WPのルート(index.php がある場所)
本文中の example.com は $SITE_DOMAIN に読み替え。
ファイル編集は root 権限または適切な sudo で実施。
1) なぜこれが「LLMO対応」なのか(要点)
- AIクローラに配慮
GPTBot / ClaudeBot / Google-Extended / Perplexity などが実際にサイトを巡回。
robots.txt / llms.txt / ai-index.json を整備し、参照して良い範囲・出典方針・機械可読の正規インデックスを提示すると、誤引用抑止・正確な取り込みにつながる。 - REST API を安定化
自動投稿や将来の RAG/外部連携は WordPress REST が土台。
リライトやプラグイン干渉で/wp-json/...が HTML 返しになる事故を避けるため、/index.php?rest_route=に統一して 常に JSON を保証 → 壊れにくい基盤に。 - ライセンスと出典の明示
記事末尾に CC BY 4.0 を自動表示(人間向け)。llms.txtとai-index.jsonでも方針とライセンスを機械可読に提示(AI向け)。
→ 安心して引用・再利用できる条件を二重に明示。
2) AIクローラ対応(robots.txt / llms.txt / ai-index.json)
2-1. robots.txt(AI含むクローラ許可+Sitemap 案内)
sudo tee "$WEBROOT/robots.txt" >/dev/null <<'EOF' User-agent: GPTBot Allow: / User-agent: ClaudeBot Allow: / User-agent: Google-Extended Allow: / User-agent: PerplexityBot Allow: / User-agent: Applebot-Extended Allow: / User-agent: * Allow: / Sitemap: https://example.com/sitemap.xml EOF # 自サイトURLに置換 sed -i "s#https://example.com/#$SITE_URL/#" "$WEBROOT/robots.txt"
2-2. llms.txt(LLM向け利用方針)
sudo tee "$WEBROOT/llms.txt" >/dev/null <<'EOF' # Location: https://example.com/llms.txt policy: allow-crawl; prefer-citation; contact: admin@example.com sitemap: https://example.com/sitemap.xml ai-index: https://example.com/ai-index.json license: https://example.com/policy EOF sed -i "s#https://example.com/#$SITE_URL/#; s#admin@example.com#admin@$SITE_DOMAIN#" "$WEBROOT/llms.txt"
2-3. ai-index.json を出す(mu-plugin で機械可読インデックス)
sudo -u www-data mkdir -p "$WEBROOT/wp-content/mu-plugins"
sudo -u www-data tee "$WEBROOT/wp-content/mu-plugins/ai-index.php" >/dev/null <<'PHP'
<?php
/**
* Plugin Name: AI Index (mu)
* Description: Publish a minimal JSON index of posts for LLMs/RAG.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
add_action('rest_api_init', function () {
register_rest_route('ai/v1', '/index', [
'methods' => 'GET',
'callback' => function () {
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 200,
'orderby' => 'modified',
'order' => 'DESC',
'post_status' => 'publish',
]);
$items = [];
while ($q->have_posts()) { $q->the_post();
$excerpt = get_the_excerpt();
if (empty($excerpt)) {
$excerpt = wp_trim_words(wp_strip_all_tags(get_the_content()), 60, '…');
}
$items[] = [
'url' => get_permalink(),
'title' => get_the_title(),
'summary' => wp_strip_all_tags($excerpt),
'lastmod' => get_the_modified_time('Y-m-d'),
'tags' => wp_get_post_tags(get_the_ID(), ['fields' => 'names']),
'license' => 'CC BY 4.0',
];
}
wp_reset_postdata();
return [
'site' => home_url(),
'updated' => date('Y-m-d'),
'items' => $items,
];
},
'permission_callback' => '__return_true',
]);
});
PHP
(任意)/ai-index.json という固定パスで出したい場合:Nginx でリライト
# /etc/nginx/sites-available/$SITE_DOMAIN の server { ... } 内に追加
location = /ai-index.json {
rewrite ^ /index.php?rest_route=/ai/v1/index last;
}
反映と確認:
sudo nginx -t && sudo systemctl reload nginx curl -I "$SITE_URL/robots.txt" curl -I "$SITE_URL/llms.txt" curl -I "$SITE_URL/ai-index.json" # 200 + application/json が目安 curl -s "$SITE_URL/ai-index.json" | head
3) REST API の安定化(/index.php?rest_route= に統一)
背景: /wp-json/... がテーマ/プラグイン/リライトで HTML を返す事故を回避。
方針: クライアント(スクリプト)からの呼び出しは すべて index.php?rest_route= を使う。
Python クライアント例(抜粋)
from requests.auth import HTTPBasicAuth
import requests, os
from markdown2 import markdown
WP_URL = os.getenv("WP_URL") # 例: https://papanda925.com
WP_USER = os.getenv("WP_USER")
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD")
def _posts_api_base():
return f"{WP_URL.rstrip('/')}/index.php?rest_route=/wp/v2/posts"
HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
def _ensure_json_response(resp):
ct = resp.headers.get("Content-Type", "")
if "application/json" not in ct:
preview = (resp.text or "")[:200].replace("\n", "\\n")
raise RuntimeError(f"Unexpected Content-Type: {ct}. Preview: {preview!r}")
def post_to_wordpress(title: str, markdown_content: str, status: str = "publish"):
html = markdown(markdown_content)
data = {"title": title, "content": html, "status": status}
r = requests.post(_posts_api_base(), headers=HEADERS,
auth=HTTPBasicAuth(WP_USER, WP_APP_PASSWORD),
json=data, timeout=30)
r.raise_for_status()
_ensure_json_response(r)
print("OK", r.json().get("id"), r.json().get("link"))
動作確認
curl -i -H "Accept: application/json" \ "$SITE_URL/index.php?rest_route=/wp/v2/posts&per_page=1" # → HTTP/2 200 + Content-Type: application/json
よくある落とし穴
- 認証/WAF系プラグインが REST を塞ぐ → ホワイトリスト/許可設定を入れる
- リダイレクト多発 →
-Lで追っても最終text/htmlならindex.php?rest_route=に切替
4) ライセンス・出典の明示(mu-plugin で末尾ボックス)
目的: 人間にも AI にも、「再利用条件」「出典表記」を明確化。
実装: 投稿本文の末尾に CC BY 4.0 告知を自動付与。管理画面/REST/固定ページや特定スラッグは除外。
sudo -u www-data tee "$WEBROOT/wp-content/mu-plugins/license-box.php" >/dev/null <<'PHP'
<?php
/**
* Plugin Name: License Box (mu)
* Description: Append a license notice box to single posts.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
function _p925_should_show_box(){
if (is_admin()) return false;
if (defined('REST_REQUEST') && REST_REQUEST) return false;
if (!is_singular('post')) return false;
if (!in_the_loop() || !is_main_query()) return false;
$exclude_slugs = ['policy','privacy-policy','about'];
$slug = get_post_field('post_name', get_queried_object_id());
if (in_array($slug, $exclude_slugs, true)) return false;
return true;
}
function _p925_box_html(){
$policy = esc_url(home_url('/policy'));
$lic = 'https://creativecommons.org/licenses/by/4.0/';
return '<div class="license-box" style="margin-top:2rem;padding:1rem;border:1px solid #e5e7eb;border-radius:.75rem;background:#fafafa">
<strong>ライセンス</strong>:本文・コードは特記なき限り <a href="'.$lic.'" target="_blank" rel="noopener">CC BY 4.0</a>。引用は出典URLを明記してください。<br>
<a href="'.$policy.'">利用ポリシー</a> も参照。
</div>';
}
add_filter('the_content', function($content){
if (!_p925_should_show_box()) return $content;
return $content . _p925_box_html();
}, 99);
PHP
確認: 任意の投稿ページ末尾に枠が表示(policy 等は除外)。
5) Nginx の最小追記と検証
# /etc/nginx/sites-available/$SITE_DOMAIN の server { ... } へ
# 固定パスでの JSON インデックス公開
location = /ai-index.json {
rewrite ^ /index.php?rest_route=/ai/v1/index last;
}
# テキスト系を明示(任意)
location = /llms.txt {
default_type text/plain;
try_files /llms.txt =404;
}
反映とヘッダ確認:
sudo nginx -t && sudo systemctl reload nginx curl -I "$SITE_URL/wp-json/" # 200 または 301→最終200 curl -I "$SITE_URL/ai-index.json" # 200 + application/json curl -I "$SITE_URL/llms.txt" # 200 + text/plain curl -I "$SITE_URL/robots.txt" # 200
6) 監視・検証コマンド(AIクローラ来訪の把握)
# 直近で AI クローラだけ抽出
zgrep -hE "GPTBot|ClaudeBot|Google-Extended|PerplexityBot|Applebot-Extended" \
/var/log/nginx/access.log* | tail -n 100
# UA ランキング(だいたいの傾向)
zgrep -h . /var/log/nginx/access.log* | \
awk -F\" '{print $6}' | sort | uniq -c | sort -nr | head
7) チェックリスト
robots.txt / llms.txt / ai-index.jsonが 200ai-index.jsonのitemsに最新記事が反映- 投稿末尾の ライセンス枠 が表示(除外ページでは出ない)
index.php?rest_route=で REST が常時 JSONnginx -tOK → リロード後ログにエラーなし- アクセスログに AIクローラUA の来訪を確認
8) ロールバック方法
- Nginx: 事前バックアップ(
*.bak.YYYY-MM-DD)へ差し替え →nginx -t && reload - mu-plugins:
ai-index.php/license-box.phpを一時リネーム(.off等)で停止 - ルートファイル:
robots.txt/llms.txtを退避または削除
9) まとめ(LLMO視点の最終要点)
- 供給:
ai-index.jsonで 軽量・正規・機械可読なデータソースを提供 - 方針:
llms.txtと 記事末尾ボックスで再利用条件・出典ルールを明示 - 耐性:
index.php?rest_route=で REST 経路を安定化(RAG/自動化の土台)
これで「AIに正しく読まれ、正しく引用される」WordPress基盤が整います。
以降は記事品質やタグ設計、更新頻度の最適化など “中身のLLMO” に集中できます。

コメント