対象読者: 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 -t
OK → リロード後ログにエラーなし- アクセスログに 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” に集中できます。
コメント