開発

Blog

開発

【WordPress】AI 検索向けに llms.txt と IndexNow をプラグインなしで足した

検索エンジンや AI の検索に、新しい記事を早く、正しく載せてもらいたい。そう思って、僕の WordPress の共通のプラグイン(NARI Base)に、llms.txt と IndexNow の2つを足しました。どちらも配布のプラグインは使わず、PHP だけで作っています。この記事では、それぞれが何をするものかと、実装のポイントをまとめます。

この記事でわかること
  • llms.txt と IndexNow が、それぞれ何をするものか(Google 検索での扱いも)
  • llms.txt を、ファイルを置かずに WordPress から返す方法
  • Yoast SEO の説明文を llms.txt に使うときの落とし穴
  • IndexNow で、公開・更新した URL を1回の保存で1回だけ送る方法

llms.txt と IndexNow の違い

llms.txt

AI にサイトの中身を短く伝えるための、Markdown のファイルです(/llms.txt に置く)。サイト名・説明・主なページの一覧などを書きます。提案されている形式で、すべての AI が読むと決まっているわけではありません。

IndexNow

記事を公開・更新したときに、その URL を検索エンジンにすぐ知らせる仕組みです。Bing や Yandex などが使っています。知らせないと、検索エンジンが次に見に来るまで、新しい記事が載りません。

Google 検索は、llms.txt も IndexNow も使っていません

llms.txt は、Google 検索(AI の機能も含む)では使われていません。Google の公式の文書にも、AI 向けのファイルを新しく作る必要はないと書かれています(2026年11月に確かめました)。Google の AI の機能に載りたいから入れる、というものではありません。

Google には IndexNow で知らせても届きません。Google には、これまでどおりサイトマップ(Yoast SEO が出しているもの)で伝えています。

llms.txt を WordPress から返す

このサイトの llms.txt は、こんな形です(一部。URL のドメインは example.com に置きかえています)。

# オレオレナリ

> WordPress・Shopify のホームページ制作、保守、攻撃を受けたサイトの復旧を、システムエンジニアの上柳直人(NARI)が個人で受けています。個人だから安く、品質は本業と同じ。全国からオンラインで、ご相談は無料です。

## 主なページ

- [オレオレナリについて](https://example.com/about/): システムエンジニアの上柳直人(NARI)のプロフィールです。…
- [サービスと料金](https://example.com/service/): WordPress・Shopify のホームページ制作、公開後の保守、…

## 制作実績

一覧:https://example.com/works/

- [Shopifyデモストア 鳴々酒造](https://example.com/works/naripify-sake/): 架空の酒蔵「鳴々酒造」の Shopify ストアです。…

公開フォルダにファイルを置くのではなく、リクエストが来たら WordPress が作って返します。記事を公開すれば、すぐに一覧に載ります。

// /llms.txt を返す
add_action( 'parse_request', function ( $wp ) {
	if ( 'llms.txt' !== $wp->request ) {
		return;
	}
	nocache_headers(); // サーバーのキャッシュ(nginx)に古い一覧を残さない
	header( 'Content-Type: text/plain; charset=utf-8' );
	header( 'X-Robots-Tag: noindex', true ); // llms.txt そのものは検索結果に出さない
	echo llms_txt(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- テキストとして返す
	exit;
} );

parse_request は、WordPress がリクエストの URL を読み取ったあとに呼ばれるアクションです。$wp->request が llms.txt のときだけ、中身を出して終わります。

nocache_headers() を付けているのは、このサイトのサーバー(お名前.com のレンタルサーバー RS)の nginx のページキャッシュに、古い一覧が残らないようにするためです。

作った結果は覚えておき、記事を保存したら作り直す

毎回すべての記事を読んで作ると重いので、作った結果を1時間だけ覚えておきます(トランジェント)。記事を保存・削除したら、覚えていたものを消して作り直します。

// 記事を保存・削除したら作り直す
foreach ( array( 'save_post', 'deleted_post', 'transition_post_status' ) as $nari_base_hook ) {
	add_action( $nari_base_hook, fn() => delete_transient( LLMS_TXT_TRANSIENT ) );
}
unset( $nari_base_hook );

説明文は Yoast SEO の meta description を使う

記事の一覧には、タイトル・URL と一緒に説明文を載せています。説明文は、Yoast SEO の説明文(meta description)を使い、なければ抜粋を使います。

function llms_txt_description( \WP_Post $post ): string {
	$description = '';
	if ( function_exists( 'YoastSEO' ) ) {
		try {
			// Yoast は記事ごとの情報を、自前のテーブル(yoast_indexable)の行の ID で覚える。
			// 本番以外の環境では行を保存しないので ID が空になり、2 件目からも 1 件目の情報が返ってくる。毎回忘れさせてから読む
			YoastSEO()->classes->get( \Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer::class )->clear();
			$meta        = YoastSEO()->meta->for_post( $post->ID );
			$description = $meta ? (string) $meta->description : '';
		} catch ( \Throwable $e ) {
			$description = '';
		}
	}
	if ( '' === $description ) {
		$description = get_the_excerpt( $post );
	}
	$description = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( html_entity_decode( $description, ENT_QUOTES, 'UTF-8' ) ) ) );
	return mb_strimwidth( $description, 0, 240, '…' );
}
ローカルでは、全部の記事が同じ説明文になった

Yoast SEO は、記事ごとの情報を自前のテーブルの行の ID で覚えています。本番以外の環境では、Yoast はその行を保存しないので ID が空になり、1件目の記事の情報が2件目以降にも返ってきていました。

そこで、記事ごとに Meta_Tags_Context_Memoizer の clear() を呼んで、覚えている情報を忘れさせてから読んでいます。

Yoast SEO の llms.txt を使わなかった理由

Yoast SEO にも、llms.txt を作る機能があります。ただ、公開フォルダに本物のファイルを書き出し、作り直すのは週に1回です。新しい記事をすぐに載せたかったので、Yoast の機能はオフに固定して、自分で作ることにしました。記事をあまり増やさないサイトなら、Yoast の機能で十分だと思います。

IndexNow で、公開・更新した URL を知らせる

IndexNow は、URL の一覧を JSON で送るだけのシンプルな仕組みです。送るときには「このサイトの持ち主である証明」として鍵を付け、同じ鍵を書いたファイルをサイトに置いておきます。検索エンジンは、そのファイルを読んで確かめます。

鍵を作って、/{鍵}.txt で返す

function indexnow_key(): string {
	$key = get_option( 'nari_base_indexnow_key' );
	if ( ! is_string( $key ) || ! preg_match( '/^[a-f0-9]{32}$/', $key ) ) {
		$key = bin2hex( random_bytes( 16 ) );
		update_option( 'nari_base_indexnow_key', $key, true );
	}
	return $key;
}
// 鍵のファイル(/{鍵}.txt)。ファイルは置かず、WordPress が返す
add_action( 'parse_request', function ( $wp ) {
	if ( indexnow_key() . '.txt' !== $wp->request ) {
		return;
	}
	header( 'Content-Type: text/plain; charset=utf-8' );
	header( 'X-Robots-Tag: noindex', true );
	echo indexnow_key(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 英数字だけ
	exit;
} );

鍵は初めて使うときに作って、オプションに保存します。鍵のファイルも llms.txt と同じで、ファイルは置かずに WordPress が返しています。

送るのは本番の環境だけ

if ( 'production' !== wp_get_environment_type() ) {
	return;
}

wp_get_environment_type() は、wp-config.php の WP_ENVIRONMENT_TYPE の値を返す関数です。ローカルでは local にしているので、ローカルで記事を公開しても送りません。

公開・更新を拾って、最後にまとめて1回だけ送る

add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
	if ( 'publish' !== $new_status || wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
		return;
	}
	if ( ! is_post_type_viewable( $post->post_type ) || 'attachment' === $post->post_type || '' !== $post->post_password ) {
		return;
	}
	// Yoast の編集画面で「検索結果に表示しない」にした記事は知らせない
	if ( '1' === get_post_meta( $post->ID, '_yoast_wpseo_meta-robots-noindex', true ) ) {
		return;
	}

	indexnow_queue( get_permalink( $post ) );

	// 新しく公開したときは、一覧ページ(/blog/ など)も変わるので一緒に知らせる
	if ( 'publish' !== $old_status ) {
		$archive = get_post_type_archive_link( $post->post_type );
		if ( $archive ) {
			indexnow_queue( $archive );
		}
	}
}, 10, 3 );

記事を公開・更新したときは、その URL をためておきます。新しく公開したときは、一覧ページ(/blog/ など)も中身が変わるので、一緒にためます。

// リクエストの最後に、まとめて 1 回だけ送る(返事は待たない)
add_action( 'shutdown', function () {
	$urls = array_filter(
		indexnow_queue(),
		function ( $url ) {
			$transient = 'nari_base_indexnow_' . md5( $url );
			if ( get_transient( $transient ) ) {
				return false;
			}
			set_transient( $transient, 1, 10 * MINUTE_IN_SECONDS );
			return true;
		}
	);
	if ( $urls ) {
		indexnow_submit( $urls );
	}
} );

ブロックエディターは、1回の保存で本文とメタボックスを別々に保存するので、何もしないと同じ URL を何度も送ってしまいます。なので、リクエストの最後(shutdown)にまとめて1回だけ送り、さらに同じ URL は10分に1回までにしています。返事は待たない(blocking を false)ので、保存が遅くなることもありません。

予約投稿にも効きます

予約投稿の記事が公開されるときも、transition_post_status は呼ばれます。なので、予約した時刻に公開された記事も、そのまま IndexNow で知らせられます。

確かめ方

llms.txt は、ブラウザで開くか、curl で中身を見ます。

curl -s https://example.com/llms.txt

IndexNow は、送る関数の $blocking を true にして呼ぶと、返事を確かめられます。200 か 202 が返ってくれば、受け付けられています。

おわりに

llms.txt も IndexNow も、入れたからといって順位が上がるものではありません。特に llms.txt は、Google 検索では使われていませんし、どの AI がどこまで読んでいるのかもはっきりしません。なので、手間をかけすぎず、軽く入れておくくらいがちょうどいいと思っています。IndexNow のほうは、Bing などに新しい記事を早く知らせられるので、入れておいて損はないです。

どちらも PHP だけで、それぞれ200行足らずで作れました。配布のプラグインを増やさずに済むので、ほかの案件にも同じものを入れやすいです。

お仕事のご相談

WordPress の SEO まわりのカスタマイズのような制作・改修のご相談も受け付けています。お気軽にどうぞ。

//サイト内を検索

制作実績・ブログ・お知らせから探します。キーワードはスペースで区切って複数入れられます。