JavaScript なしで本文が読めないと、AI に引用されない
AI クローラーの多くは JavaScript を実行しません。React や Vue で作った SPA は、ブラウザでは正しく表示されていても、AI から見ると本文がほとんど空の HTML です。なぜそうなるのか、自分のサイトが該当するかの確かめ方、Next.js や Nuxt での直し方までを順に説明します。
なぜ AI クローラーは JavaScript を実行しないのですか?
主な理由はコストです。HTML を取得するだけなら 1 ページあたり数ミリ秒で済みますが、ヘッドレスブラウザを起動して JavaScript を実行すると、CPU 時間もメモリも桁違いに増えます。数十億ページを対象にする規模では、この差がそのまま実行可能かどうかの境界になります。
クロールというのは、突き詰めると「1 ページあたりのコスト × ページ数」です。ページ数が数十億という単位になると、1 ページあたりの コストがわずかに増えるだけで、必要な計算資源が現実的でない量まで膨らみます。素の HTTP リクエストとヘッドレスブラウザの起動では、この 1 ページあたりのコストが比較になりません。
| HTML を取得するだけ | ヘッドレスブラウザで描画 | |
|---|---|---|
| やること | 1 回の GET でバイト列を受け取る | GET に加えて JS・CSS・画像を取得し、JS を実行して DOM を構築する |
| 追加のリクエスト | 0 件 | 数十〜数百件(バンドル・API・フォント等) |
| 必要なメモリ | 数 MB | 1 タブあたり数百 MB |
| 失敗要因 | ほぼネットワークのみ | JS エラー・タイムアウト・API 側の失敗など多数 |
Googlebot は例外的に JavaScript を実行します。ただしそれは、まず HTML をインデックスし、後からレンダリング用のキューに回して再度処理する二段階インデックスという仕組みを、Google が長年かけて構築してきたからです。検索エンジンとして数十年の投資がある会社の設備であり、 「今どきのクローラーなら当然できること」ではありません。
公開されている情報と、断定できないこと
正直に書きます。GPTBot や ClaudeBot が JavaScript を実行するかどうかは、各社とも仕様として明確に公開していません。「実行しない」と断言する記事をよく見かけますが、根拠が示されていることは稀ですし、仮に今そうだとしても将来変わりえます。ここは推測で埋めるべき場所ではありません。
公開情報から確実に言えるのは、次の 3 点です。
- Common Crawl(CCBot)は JavaScript を実行しません。取得した生の HTML をアーカイブする静的なクロールで、多くの LLM の学習データの供給元になっています
- ユーザー操作時に取得するタイプのクローラーは、待ち時間の制約が厳しい。
ChatGPT-UserやPerplexity-Userのように、人が質問した瞬間にページを取りに行く用途では、応答時間が数秒に収まる必要があります。 レンダリングに割ける余裕は学習用クロール以上に小さくなります - 素の HTML に本文が無ければ、JS を実行しないクローラーからは絶対に読めません。これは相手の実装に依存しない、こちら側で確定できる事実です
自分のサイトが該当するか、どうやって確かめますか?
curl で HTML を取得し、本文の文字列が含まれているかを確認するのが最短です。ブラウザの検証ツールに映っているのは JavaScript 実行後の DOM であり、クローラーが受け取るものとは別物です。必ず生の HTTP レスポンスを見てください。
ここを間違えると調査そのものが無意味になります。DevTools の Elements タブは、JS が実行され DOM が組み上がった後の姿です。クローラーが見るのはその手前、サーバーが返したバイト列そのものです。
# 1) JS を実行せずに HTML を保存する(= クローラーが受け取るのと同じもの)
curl -sL https://example.com/ -o page.html
# 2) バイト数を見る。CSR の SPA だと 2〜5 KB 程度しかないことが多い
wc -c page.html
# 3) 画面に見えているはずの本文が HTML に入っているか確認する
grep -c "トップページに表示されている見出しの文字列" page.html
# → 0 なら、その文字列は JS 実行後にしか存在しないCSR のみで作られたサイトの HTML は、だいたい次のような姿になっています。title は入っていても、本文と呼べるものはどこにもありません。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>製品情報 | Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/index-a1b2c3d4.js"></script>
</body>
</html>この HTML から読み取れる情報は、タイトルの十数文字だけです。どれだけ良い記事を書いても、AI から見れば「タイトルだけがあって中身が空のページ」であり、引用のしようがありません。
ブラウザだけで確認する方法
- Chrome で対象ページを開き、
Ctrl + U(Mac はCmd + Option + U)でソースを表示します。これは JS 実行前の HTML です - そこで
Ctrl + Fを押し、本文中の特徴的な一文を検索します。見つからなければ、その本文は素の HTML に存在していません - より実感を得たい場合は、DevTools を開いて
Ctrl + Shift + Pから「Disable JavaScript」を実行し、リロードします。真っ白になれば CSR です
Next.js ではどう直しますか?
App Router は既定でサーバーコンポーネントなので、多くの場合は本文コンポーネントから "use client" を外すだけで解決します。本質的な作業は、useEffect 内の fetch でデータを取っている箇所を、サーバー側の取得に移すことです。
典型的なアンチパターンはこの形です。ブラウザでは問題なく動きますが、素の HTML には「読み込み中...」しか出力されません。
"use client";
import { useEffect, useState } from "react";
export default function Post({ params }: { params: { id: string } }) {
const [post, setPost] = useState<Post | null>(null);
// この fetch はブラウザでしか動かない。
// クローラーが受け取る HTML には <p>読み込み中...</p> だけが入る
useEffect(() => {
fetch("/api/posts/" + params.id)
.then((r) => r.json())
.then(setPost);
}, [params.id]);
if (!post) return <p>読み込み中...</p>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}サーバーコンポーネントに寄せると、同じ内容が HTML に焼き込まれた状態で配信されます。"use client" と useState / useEffect が消え、コード自体もむしろ短くなります。
import { getPost } from "@/lib/posts";
// "use client" を付けない = サーバーコンポーネント。
// await の結果が HTML に入った状態で配信される
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}ページ全体をサーバー化する必要はありません。本文はサーバー、操作が必要な部品だけクライアントという切り分けが基本です。 いいねボタンやタブ切り替えのような部品は "use client" のままで構いません。問題になるのは、本文そのものがクライアント側にある場合だけです。
あわせて確認したい設定
next/dynamicのssr: falseを本文に使わない。この指定は「サーバーでは描画しない」という意味なので、狙って本文を消していることになります- Pages Router なら
getStaticProps(静的生成)またはgetServerSideProps(リクエスト時生成)を使う。どちらも使っていないページは CSR です - 内容が変わらないページは静的に配ると速くて安い。App Router では
export const revalidate = 3600のような指定で、生成済み HTML を再利用できます
Nuxt やその他のフレームワークではどうしますか?
Nuxt は ssr: true(既定値)を維持し、useAsyncData / useFetch でデータを取れば SSR されます。難しいのは Vite + React のような素の SPA で、こちらはプリレンダリングか SSG の導入が必要になり、実質的な作り替えになります。
export default defineNuxtConfig({
// 既定値は true。false にすると SPA になり、AI から本文が読めなくなる
ssr: true,
// サーバーを立てずに静的 HTML として吐き出す場合(nuxt generate)
nitro: {
prerender: {
crawlLinks: true, // リンクを辿って全ページを静的化する
routes: ["/"],
},
},
});Nuxt でありがちな失敗は、onMounted の中で fetch してしまうことです。onMounted はブラウザでしか走らないため、SSR を有効にしていても、そのデータだけは HTML に入りません。データ取得は useAsyncData か useFetch を使ってください。
| 構成 | 既定の挙動 | やること |
|---|---|---|
| Next.js App Router | サーバーコンポーネント(HTML に入る) | 本文から "use client" を外す |
| Next.js Pages Router | 指定しなければ CSR | getStaticProps / getServerSideProps を足す |
| Nuxt 3 / 4 | SSR(HTML に入る) | ssr: false にしていないか確認する |
| SvelteKit | SSR(HTML に入る) | export const ssr = false を消す |
| Astro | 静的 HTML(HTML に入る) | 対応不要。client:only を本文に使わない |
| Vite / CRA の SPA | CSR(HTML は空) | プリレンダリング・SSG の導入、または構成変更 |
| WordPress・静的サイト | HTML に入っている | 対応不要 |
SSR にすれば必ず AI に引用されますか?
いいえ。SSR は「読める状態にする」ための前提条件であって、引用されるための十分条件ではありません。読めるようになった上で、robots.txt で許可されていること、内容が信頼できること、回答として抜き出しやすい構造であることが揃って、ようやく候補になります。
AI に引用されるまでには、少なくとも 3 つの段階があります。SSR 化はこのうち最初の 1 段目にすぎません。
- 読めるか — 素の HTML に本文があるか。この記事の対象です
- 入っていいか — robots.txt でそのクローラーを拒否していないか。GPTBot・ClaudeBot を許可すべきか、拒否すべきかで扱います
- 引用したくなるか — 質問に対する答えの形になっているか、誰がいつ書いたか分かるか。AI に回答として引用される文章の構造で扱います
順序が重要です。1 段目が欠けている状態で 3 段目を作り込んでも、そもそも読まれていないので何も起きません。逆に 1 段目だけ直しても、内容が薄ければ引用はされません。
どこから手を付けるべきですか?
まず測ってください。JavaScript なしで本文が何文字読めるかを確認し、1,000 字を大きく下回るようなら SSR / SSG 化が最優先です。十分に読めているなら、この記事の対策は不要で、robots.txt と本文構造の見直しに進むのが効率的です。
- 実測する —
curlか本ツールで、JS なしの本文文字数を確認します。ここが十分なら、この記事の作業は不要です - 影響範囲を絞る — 全ページを一度に直す必要はありません。検索から入ってきてほしいページ(記事・製品情報・料金)から着手します。 管理画面やダッシュボードは CSR のままで構いません
- 本文と操作部品を分ける — 本文はサーバー、操作が必要な部分だけクライアントに切り分けます
- 直したら測り直す — 本文文字数が増えたことを、同じ方法で確認します
この作業は SEO のためだけではありません。素の HTML に本文が入っているページは、初回表示が速く、JS が失敗しても内容が見え、SNS のカード生成にも強くなります。AI 対応は結果のひとつであって、やっていること自体は昔からの Web の基本です。