本文へスキップ
サイト診断

JavaScript なしで本文が読めないと、AI に引用されない

AI クローラーの多くは JavaScript を実行しません。React や Vue で作った SPA は、ブラウザでは正しく表示されていても、AI から見ると本文がほとんど空の HTML です。なぜそうなるのか、自分のサイトが該当するかの確かめ方、Next.js や Nuxt での直し方までを順に説明します。

執筆: 公開:

なぜ AI クローラーは JavaScript を実行しないのですか?

主な理由はコストです。HTML を取得するだけなら 1 ページあたり数ミリ秒で済みますが、ヘッドレスブラウザを起動して JavaScript を実行すると、CPU 時間もメモリも桁違いに増えます。数十億ページを対象にする規模では、この差がそのまま実行可能かどうかの境界になります。

クロールというのは、突き詰めると「1 ページあたりのコスト × ページ数」です。ページ数が数十億という単位になると、1 ページあたりの コストがわずかに増えるだけで、必要な計算資源が現実的でない量まで膨らみます。素の HTTP リクエストとヘッドレスブラウザの起動では、この 1 ページあたりのコストが比較になりません。

HTML 取得とレンダリングのコスト比較
HTML を取得するだけヘッドレスブラウザで描画
やること1 回の GET でバイト列を受け取るGET に加えて JS・CSS・画像を取得し、JS を実行して DOM を構築する
追加のリクエスト0 件数十〜数百件(バンドル・API・フォント等)
必要なメモリ数 MB1 タブあたり数百 MB
失敗要因ほぼネットワークのみJS エラー・タイムアウト・API 側の失敗など多数

Googlebot は例外的に JavaScript を実行します。ただしそれは、まず HTML をインデックスし、後からレンダリング用のキューに回して再度処理する二段階インデックスという仕組みを、Google が長年かけて構築してきたからです。検索エンジンとして数十年の投資がある会社の設備であり、 「今どきのクローラーなら当然できること」ではありません。

公開されている情報と、断定できないこと

正直に書きます。GPTBot や ClaudeBot が JavaScript を実行するかどうかは、各社とも仕様として明確に公開していません。「実行しない」と断言する記事をよく見かけますが、根拠が示されていることは稀ですし、仮に今そうだとしても将来変わりえます。ここは推測で埋めるべき場所ではありません。

公開情報から確実に言えるのは、次の 3 点です。

  • Common Crawl(CCBot)は JavaScript を実行しません。取得した生の HTML をアーカイブする静的なクロールで、多くの LLM の学習データの供給元になっています
  • ユーザー操作時に取得するタイプのクローラーは、待ち時間の制約が厳しい。ChatGPT-UserPerplexity-User のように、人が質問した瞬間にページを取りに行く用途では、応答時間が数秒に収まる必要があります。 レンダリングに割ける余裕は学習用クロール以上に小さくなります
  • 素の HTML に本文が無ければ、JS を実行しないクローラーからは絶対に読めません。これは相手の実装に依存しない、こちら側で確定できる事実です

自分のサイトが該当するか、どうやって確かめますか?

curl で HTML を取得し、本文の文字列が含まれているかを確認するのが最短です。ブラウザの検証ツールに映っているのは JavaScript 実行後の DOM であり、クローラーが受け取るものとは別物です。必ず生の HTTP レスポンスを見てください。

ここを間違えると調査そのものが無意味になります。DevTools の Elements タブは、JS が実行され DOM が組み上がった後の姿です。クローラーが見るのはその手前、サーバーが返したバイト列そのものです。

terminal
# 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 は入っていても、本文と呼べるものはどこにもありません。

AI から見た SPA の HTML(実質これだけ)
<!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 から見れば「タイトルだけがあって中身が空のページ」であり、引用のしようがありません。

ブラウザだけで確認する方法

  1. Chrome で対象ページを開き、Ctrl + U(Mac は Cmd + Option + U)でソースを表示します。これは JS 実行前の HTML です
  2. そこで Ctrl + F を押し、本文中の特徴的な一文を検索します。見つからなければ、その本文は素の HTML に存在していません
  3. より実感を得たい場合は、DevTools を開いて Ctrl + Shift + P から「Disable JavaScript」を実行し、リロードします。真っ白になれば CSR です

Next.js ではどう直しますか?

App Router は既定でサーバーコンポーネントなので、多くの場合は本文コンポーネントから "use client" を外すだけで解決します。本質的な作業は、useEffect 内の fetch でデータを取っている箇所を、サーバー側の取得に移すことです。

典型的なアンチパターンはこの形です。ブラウザでは問題なく動きますが、素の HTML には「読み込み中...」しか出力されません。

app/posts/[id]/page.tsx(NG: 本文がクライアント取得)
"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 が消え、コード自体もむしろ短くなります。

app/posts/[id]/page.tsx(OK: サーバーで取得して HTML に含める)
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/dynamicssr: false を本文に使わない。この指定は「サーバーでは描画しない」という意味なので、狙って本文を消していることになります
  • Pages Router なら getStaticProps(静的生成)または getServerSideProps(リクエスト時生成)を使う。どちらも使っていないページは CSR です
  • 内容が変わらないページは静的に配ると速くて安い。App Router では export const revalidate = 3600 のような指定で、生成済み HTML を再利用できます

Nuxt やその他のフレームワークではどうしますか?

Nuxt は ssr: true(既定値)を維持し、useAsyncData / useFetch でデータを取れば SSR されます。難しいのは Vite + React のような素の SPA で、こちらはプリレンダリングか SSG の導入が必要になり、実質的な作り替えになります。

nuxt.config.ts
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指定しなければ CSRgetStaticProps / getServerSideProps を足す
Nuxt 3 / 4SSR(HTML に入る)ssr: false にしていないか確認する
SvelteKitSSR(HTML に入る)export const ssr = false を消す
Astro静的 HTML(HTML に入る)対応不要。client:only を本文に使わない
Vite / CRA の SPACSR(HTML は空)プリレンダリング・SSG の導入、または構成変更
WordPress・静的サイトHTML に入っている対応不要

SSR にすれば必ず AI に引用されますか?

いいえ。SSR は「読める状態にする」ための前提条件であって、引用されるための十分条件ではありません。読めるようになった上で、robots.txt で許可されていること、内容が信頼できること、回答として抜き出しやすい構造であることが揃って、ようやく候補になります。

AI に引用されるまでには、少なくとも 3 つの段階があります。SSR 化はこのうち最初の 1 段目にすぎません。

  1. 読めるか — 素の HTML に本文があるか。この記事の対象です
  2. 入っていいか — robots.txt でそのクローラーを拒否していないか。GPTBot・ClaudeBot を許可すべきか、拒否すべきかで扱います
  3. 引用したくなるか — 質問に対する答えの形になっているか、誰がいつ書いたか分かるか。AI に回答として引用される文章の構造で扱います

順序が重要です。1 段目が欠けている状態で 3 段目を作り込んでも、そもそも読まれていないので何も起きません。逆に 1 段目だけ直しても、内容が薄ければ引用はされません。

どこから手を付けるべきですか?

まず測ってください。JavaScript なしで本文が何文字読めるかを確認し、1,000 字を大きく下回るようなら SSR / SSG 化が最優先です。十分に読めているなら、この記事の対策は不要で、robots.txt と本文構造の見直しに進むのが効率的です。

  1. 実測するcurl か本ツールで、JS なしの本文文字数を確認します。ここが十分なら、この記事の作業は不要です
  2. 影響範囲を絞る — 全ページを一度に直す必要はありません。検索から入ってきてほしいページ(記事・製品情報・料金)から着手します。 管理画面やダッシュボードは CSR のままで構いません
  3. 本文と操作部品を分ける — 本文はサーバー、操作が必要な部分だけクライアントに切り分けます
  4. 直したら測り直す — 本文文字数が増えたことを、同じ方法で確認します

この作業は SEO のためだけではありません。素の HTML に本文が入っているページは、初回表示が速く、JS が失敗しても内容が見え、SNS のカード生成にも強くなります。AI 対応は結果のひとつであって、やっていること自体は昔からの Web の基本です。

関連する解説

自分のサイトは、実際どうなっているか

この記事で説明した項目は、すべて無料の診断ツールで実測できます。URL を入れるだけ・登録不要です。

無料で診断する