積み上げログ技術ブログ記事一覧へ

自主執筆記事

ReactのトップページはHTMLソースが空だった|生成AIのクローラーには真っ白に見えている

Vite + React で作ったトップページを curl で取得したら、リンクが1つも入っていなかった。div id=root が空で、中身はすべてJavaScriptが描いていた。Googleは実行してくれるが、生成AIのクローラーの多くは実行しない。何が見えていなかったのか、どう直したのかを書く。

SEO公開: 2026/08/24更新: 2026/08/24
ReactSPASEOLLMOllms.txt構造化データ

01 / 本文

結論:HTMLソースを見たら、リンクが0件だった

運営しているサイトのトップページに、SEO対策をしようとした。まず現状を測ろうと思って、curl で取得してリンクを数えた。

0件だった。

Vite + React で作っていて、<div id="root"></div> が空のまま配信されていた。見出しも、説明文も、ナビゲーションも、すべてJavaScriptが後から描いていた。

ブラウザで開けば普通に表示される。だから気づかなかった。

curl -sSL https://example.com/ | grep -c 'href="/'
# → 0

curl -sSL https://example.com/ | grep -o '<div id="root">.*</div>'
# → <div id="root"></div>

02 / 本文

Googleは実行する。生成AIのクローラーは実行しないことが多い

Googlebot は JavaScript を実行する。だから SPA でも検索には載る(載るまでに時間はかかる)。

問題は、それ以外のクローラーだ。生成AIが参照するために巡回するクローラーの多くは、HTMLを取得するだけでJavaScriptを実行しない。

つまり、AIから見た私のトップページは真っ白なページだった。サイトの説明も、どこに何があるかも、何も伝わっていなかった。

検索経由の流入だけを考えていれば問題は表面化しない。しかし生成AIに引用される経路を考えると、これは致命的だった。

03 / 本文

直し方:#root の中に、静的な中身を書いておく

サーバーサイドレンダリングに移行するのが本筋だが、静的サイトとして配信している都合上、そこまでは変えたくなかった。

採った方法は単純だ。<div id="root"> の中に、要点と主要な導線を静的に書いておく。

React の createRoot はマウント時に中身を置き換えるので、JavaScript が動けば消える。見た目は何も変わらない。

JavaScript を実行しないクローラーには、この静的な中身が見える。

<body>
  <!-- React が読み込まれるまでの中身。JSを実行しないクローラーが
       見るのはここなので、要点と主要な導線を静的に置いておく。
       描画が始まると React がまるごと置き換える。 -->
  <div id="root">
    <main>
      <header>
        <p>サイト名</p>
        <nav>
          <a href="/index-a/">索引A</a>
          <a href="/index-b/">索引B</a>
        </nav>
      </header>
      <section>
        <h1>このサイトが何をするか</h1>
        <p>どんなデータを、どこから取って、何を載せているか。
           載せていないものは何か。</p>
      </section>
    </main>
  </div>
  <script type="module" src="/src/main.jsx"></script>
</body>

04 / 本文

あわせて入れたもの

静的な中身を置いたついでに、AI向けの手当てを3つ入れた。

1. 構造化データを厚くした。 WebSite だけだったところに Dataset を足し、何を収録しているか、どこから取ったか、独自の評価を含まないことを機械可読な形で書いた。一覧ページには CollectionPageItemList、詳細ページには BreadcrumbList を入れた。

2. llms.txt を置いた。 サイトの要約、載せているもの、載せていないもの、数え方の制約を書いたテキストファイルをルートに置く。標準化の途上だが、置くコストは低い。

3. robots.txt でAIのクローラーを明示的に許可した。 GPTBotClaudeBotPerplexityBotGoogle-Extended などを列挙した。載せているのは公開情報だけなので、隠す理由がない。

# public/llms.txt の骨組み

# サイト名(example.com)

> 1〜2文で、何をするサイトかを書く。

## このサイトが載せているもの
- 具体的な項目を箇条書きで

## 載せていないもの
- 推測や独自の評価は含まない、など

## 数え方の制約
- 上限やサンプリングがあるなら、正直に書く

## 主なページ
- URLと、それが何かの説明

## 出典
- 一次情報のURL

05 / 本文

llms.txt に「載せていないもの」を書く

llms.txt で一番効くと感じたのは、載せていないものと、数え方の制約を書く部分だ。

私のサイトでは、集計に上限がある。「実際より少なく出ることがある」と明記した。AIが数字を引用するとき、この但し書きまで伝わってほしい。

「推測や独自の評価は含まない」と書くのも同じ理由だ。どこまでが事実で、どこからが解釈でないかを先に宣言しておく。

これは SEO というより、誤って引用されないための予防だと思っている。

06 / 本文

確認のしかた

対策したあと、JavaScript を実行しない状態で何が見えるかを確かめる。curl で取って数えるだけでいい。

# 主要な導線がHTMLに入っているか
curl -sSL https://example.com/ | grep -c 'href="/'

# 構造化データが出力されているか
curl -sSL https://example.com/ | grep -c 'application/ld+json'

# llms.txt が配信されているか
curl -sS -o /dev/null -w '%{http_code}
' https://example.com/llms.txt

07 / 本文

まとめ

SPA のHTMLソースは空になる。 ブラウザで見えているものと、クローラーが受け取るものは違う。

Googlebot は JavaScript を実行するが、生成AIのクローラーの多くは実行しない。AIからの参照を考えるなら、HTMLに中身が要る。

サーバーサイドレンダリングに移せないなら、#root の中に静的な中身を置くだけでも効く。React が置き換えるので、見た目は変わらない。

llms.txt には、載せているものだけでなく 載せていないものと数え方の制約を書く。誤った引用を防ぐ側に効く。

そして、対策したら curl で取って数える。ブラウザで確認しても意味がない。

コメント

この記事へのコメント

お名前だけで投稿できます。アカウント登録やログインは不要です。 いただいたコメントは確認のうえ公開します。