IT技術ブログ

LLMO・AIOの観点を、ビルド後のHTMLで点検するスクリプト

ビルド後のHTMLを読み、AIクローラーごとのrobots.txtの許可、nosnippet、静的HTMLの本文、構造化データと見える内容、llms.txtを、公式の根拠つきで点検するスクリプトです。このブログの結果も載せます。

この記事の結論:LLMO・AIOの点検は、AI検索に出る前提を自分で壊していないかを、ビルド後のHTMLで機械的に確かめるところまでできます。見るのは、①robots.txtでAIのクローラーを止めていないか、②nosnippetなどでAI機能に本文を使わせない指定をしていないか、③本文が静的なHTMLに入っているか、④構造化データが見えている内容と一致しているか、⑤llms.txtのURLが正しいか、⑥IndexNowのキーファイルがあるか、の6つです。どの項目も、Google・OpenAI・Anthropicなどの公式の文書を根拠にし、確認日(2026年10月4日)と出典のURLをスクリプトと出力の両方に残しています。根拠が無い自社の書き方の決まりは「目安」として分けました。

このブログ(Astroで書き出し、268ページ)で流したところ、エラーは0件、注意は76件、目安は69件、情報は20件でした。AIクローラーは14の名前(Googlebotを足して15)すべてが許可で、nosnippetの指定も0件です。注意の中身は、すでにSEO一括点検の記事で出ていた短い説明文の69件と、本文の文字が少ない一覧ページ7件です。最初に流したときは一覧ページが23件引っかかったので、記事が2本以下のタグの一覧をnoindexにしてから流し直しました。この記事では、追加した点検の中身と根拠、実際の結果、わざと壊したコピーでの確かめ、ファイルを読むだけでは分からないことを書きます。

AI検索のために特別な作業は要らないと、Googleは書いている

GoogleのAI Overviews(AIによる概要)とAIモードに、追加の技術要件はありません。Googleの「AI Features and Your Website」には、補足のリンクとして出るには、ページがインデックスされ、スニペット付きで検索に表示できる状態であることが条件で、それ以外の要件は無いと書かれています。

そのため、このスクリプトでLLMO・AIO向けに足した点検の多くは、新しい施策ではありません。SEOの前提を、AI検索の側から見直すものです。robots.txtで読み取りを止めていないか、スニペットを禁止していないか、本文がHTMLに入っているか。どれも、壊れていればAIの答えにも検索の結果にも出ません。

同じ文書には、robots.txtだけでなく、CDNやホスティングの側でもクロールを許可しておくよう書かれています。CDNの設定はファイルを読んでも分からないので、最後の節で扱います。

追加した点検と、それぞれの根拠

点検は、先に公開したSEO一括点検のスクリプトに足しました。titleやcanonicalなどの点検はそのまま残し、結果を区分ごとにまとめて出すように変えています。区分は4つで、「エラー」は公式の文書が避けるよう書いているもの、「注意」は直すかを人が決めるもの、「情報」は方針として選べるものの状態、「目安」は公式の根拠が無い自社の決まりです。

項目 見つけ方 区分 根拠(2026年10月4日に確認)
AIクローラーの許可・拒否 dist/robots.txtを読み、名前ごとに当てはまるグループの規則で、トップと全ページを判定 検索用を止めていたら注意、学習用は情報 OpenAI・Anthropic・Perplexity・Google・Apple・Common Crawl・Metaのクローラーの説明
nosnippet・max-snippet:0 metaのrobotsとgooglebot、_headersのX-Robots-Tag 注意 Googleのrobots metaタグの仕様
data-nosnippet 属性の数を数える 情報 同上
本文が静的なHTMLにあるか <main>(無ければ<article>)から、ナビ・目次・ヘッダー・フッターを除いた文字数が200字未満か 注意(200字は自社の目安) GoogleのJavaScript SEOの基本
FAQPageの質問がページに見えているか 質問の文が、見える文字の中にあるか エラー Googleの構造化データの一般的なガイドライン
Articleのauthor・日付 author(とname)、datePublished、dateModified 注意(dateModifiedは情報) Googleの記事の構造化データ
Person・OrganizationのsameAs sameAsがあるか 情報 同上と、組織の構造化データ
llms.txt・llms-full.txt 先頭がH1か、中のURLがdistにあり、noindexでないか 注意 llmstxt.orgの提案と、Googleの生成AI機能向けのガイド
IndexNowのキーファイル 一番上に{キー}.txtがあり、中身がキーと同じか 情報 indexnow.orgの説明
冒頭の答え・h2の直後 最初の段落の1文目が120字以内か、h2のすぐ次が段落か 目安 公式の根拠なし(自社の書き方の決まり)

AIクローラーは、検索用と学習用で区分を変える

robots.txtの点検は、止めていることを悪いとは決めつけず、すべて報告します。学習に使われたくないから学習用を止める、というのは選べる方針だからです。ただし、検索用の名前を止めていたら「注意」にしています。各社が、止めると答えに出なくなると書いているからです。

名前 何を決めるか(各社の説明の要約) 止めたときの区分
Googlebot Google検索のクロール。AI OverviewsとAIモードもこれで決まる 注意
Google-Extended Geminiの学習と、一部のグラウンディングに使ってよいか。検索の掲載・順位には影響しない 情報
GPTBot OpenAIの基盤モデルの学習 情報
OAI-SearchBot ChatGPTの検索の答えに出すか。止めると答えに出ない 注意
ChatGPT-User 利用者の操作で取りに来る。robots.txtが適用されないことがある 情報
ClaudeBot Anthropicのモデルの学習 情報
Claude-SearchBot 検索の質の向上。止めると検索での見え方が下がりうる 注意
Claude-User 利用者の質問に答えるための取得 情報
PerplexityBot Perplexityの検索結果に出してリンクする。学習には使わない 注意
Perplexity-User 利用者の質問に答えるための取得。原則robots.txtを無視する 情報
Applebot-Extended Appleの基盤モデルの学習に使うか 情報
Bingbot Bingの検索のクローラー 注意
CCBot Common Crawlのクローラー 情報
meta-externalagent Metaの基盤モデルの学習や製品の改善 情報
Bytespider 公式の説明文書を確認できなかった 情報

確認しきれなかったものも、そのまま書いておきます。BingのクローラーのヘルプページはJavaScriptで本文を表示する作りで、2026年10月4日に取得したHTMLからは本文を読めませんでした。Bytespiderは、公式の説明文書の場所を確かめられませんでした。どちらも、スクリプトの出力にその旨を出しています。

robots.txtの読み方は、RFC 9309に合わせました。名前は大文字小文字を区別せずに探し、その名前のグループがあればそれだけを使い、無ければUser-agent: *のグループを使います。名前を指定したグループには*の規則が引き継がれない点は、AIクローラーのrobots.txt設定例の記事で書いたとおりです。複数の規則が当てはまるときは、いちばん長く一致した規則を使い、同じ長さならallowを優先します。

nosnippetとdata-nosnippetは、AI Overviewsにも効く

Googleのrobots metaタグの仕様では、nosnippetを付けたページは、AI OverviewsとAIモードの直接の入力にも使われなくなると書かれています。max-snippetも、使われる量を制限します。max-snippet:0はnosnippetと同じ扱いです。

付けるのには理由があるはずなので、インデックス可のページにnosnippetかmax-snippet:0があれば「注意」、data-nosnippetは「情報」として場所の数だけを出します。Googleがdata-nosnippetを認めるのはspan・div・sectionの要素なので、ほかの要素に付いていても、点検では数を出すだけにしています。noindexのページは、もともと検索に出さないので見ていません。

本文が静的なHTMLに入っているかは、文字数で見る

GoogleのJavaScript SEOの基本には、サーバー側で描画するか事前に描画しておくのはよい考えで、その理由の一つとして、すべてのボットがJavaScriptを実行できるわけではないと書かれています。

そこで、<main>(無ければ最初の<article>)の文字から、ナビ・目次・ヘッダー・フッター・asideを除いて数え、200字未満なら「注意」にしています。200字という線は自社の目安で、Googleが決めた数ではありません。JavaScriptだけで本文を描画しているページだけでなく、中身の薄いページも同じ網にかかります。

構造化データが、見えている内容と合っているか

Googleの構造化データの一般的なガイドラインには、ページの読者に見えていない内容をマークアップしないとあります。FAQPageは、質問の文がページの見える文字の中にあるかを確かめ、無ければ「エラー」にしています。答えは、リンクなどの書式の差で文字がずれることがあるので、先頭の40字で照合して「注意」にとどめました。

比べる前に、空白と、改行の調整に使っている見えない文字(ゼロ幅スペースなど)を消しています。このブログは文節で折り返すために本文に見えない区切りを入れているので、そのまま比べると、すべての質問が見えていないことになってしまいます。

Article・BlogPosting・NewsArticleは、Googleの記事の構造化データの説明で推奨されている、author(とそのname)、datePublished、dateModifiedがあるかを見ます。この説明には必須のプロパティが無く、dateModifiedは「より正確な日付を伝えたい場合に足す」という書き方なので、dateModifiedが無いものは「情報」にしました。著者を見分けるのにsameAsとurlの両方を使えるとも書かれているので、PersonとOrganizationのsameAsが無いものも「情報」で出します。

llms.txtは提案で、Google検索は使わない

llms.txtは、llmstxt.orgで公開されている提案です。現在の版(v2)では、サイト名のH1だけが必須とされています。Googleの生成AI機能向けのガイドには、Google検索はllms.txtを使わず、置いても検索での見え方や順位に良くも悪くも影響しないとあります。

それでもこのブログが置いているのは、ほかのサービスのためです(作り方はllms.txtをビルドで自動生成する記事に書いています)。置くのであれば、中のURLは正しくしておきたいので、先頭がH1か、自サイトのURLがdistにあるか、noindexのページが混ざっていないかを見ています。llms-full.txtは、現在の提案には定めが無いので、その旨を「情報」で出します。

IndexNowのキーファイル

IndexNowは、キーを書いたテキストファイルをサイトに置き、そのキーを添えて変更したURLを検索エンジンに知らせる仕組みです。indexnow.orgの説明では、キーは8〜128字の英数字とハイフンで、一番上に{キー}.txtとして置きます(keyLocationで別の場所を指定することもできます)。スクリプトは、一番上にあって中身がファイル名と同じ.txtを探すだけです。送信の仕組みはIndexNowを差分で送る記事に書いています。

公式の根拠が無いものは「目安」として分けた

このブログでは、記事の冒頭で問いに答え、見出しのすぐ後に文章を置く、という書き方を決めています。ただ、これはGoogleの決まりではありません。Googleの生成AI機能向けのガイドには、AIのために文章を細かく分ける必要は無いとまで書かれています。

なので、最初の段落の1文目が120字を超えるか、h2のすぐ次が段落でないかは、「目安(公式の根拠なし・自社の目安)」という別の区分にして、エラーや注意と混ぜないようにしました。

追加した部分のスクリプト

ページを読むところで、点検に使う値を集めておきます。全体は、先の記事のスクリプトに、下の2つの部分を足したものです。

site-audit.mjs(ページごとに読むところに追加)js
// ---- LLMO・AIO 用に読んでおくもの ----
const dataNosnippet = $('[data-nosnippet]').length;
// 見える文字(script・style・noscript・template を除いた body の文字)
const $v = cheerio.load(html);
$v('script, style, noscript, template, [hidden]').remove();
const visibleText = norm($v('body').text());
// 本文エリア:<main>、無ければ最初の<article>。ナビ・目次・ヘッダー・フッター・aside は除いて数える
$v('nav, header, footer, aside, .toc').remove();
const $main = $v('main').first();
const hasMainOrArticle = $v('main, article').length > 0;
const mainText = norm(($main.length ? $main : $v('article').first()).text());
// 記事本文の入れ物(このブログでは article 内の .prose。他のサイトでは作りに合わせて変える)
const $body = $('article .prose, article [itemprop="articleBody"], .post-content, .entry-content').first();
const firstP = $body.length ? $body.children('p').first().text().trim() : '';
const h2s = $body.length
  ? $body.children('h2').map((_, el) => ({ text: $(el).text().trim(), next: $(el).nextAll().first().prop('tagName')?.toLowerCase() ?? '(無し)' })).get()
  : [];
site-audit.mjs(robots.txt・スニペット・本文・構造化データの点検)js
// ---- 8. robots.txt:AIのクローラーごとに許可・拒否を出す(情報。学習用を止めるのは方針として選べる) ----
CAT = 'AIクローラー(robots.txt)';
// 各トークンが何を制御するか(各社の公式文書の要約。2026-10-04 確認)
const BOTS = [
  { token: 'Googlebot', role: 'search', what: 'Google検索のクロール。AI Overviews / AI Mode もこれで制御', src: SRC.gAiFeatures },
  { token: 'Google-Extended', role: 'training', what: 'Geminiの学習と一部のグラウンディング用。Google検索の掲載・順位には影響しない', src: SRC.gCommon },
  { token: 'GPTBot', role: 'training', what: 'OpenAIの基盤モデルの学習用', src: SRC.openai },
  { token: 'OAI-SearchBot', role: 'search', what: 'ChatGPTの検索結果に出すためのもの。拒否するとChatGPTの検索の答えに出ない', src: SRC.openai },
  { token: 'ChatGPT-User', role: 'user', what: 'ユーザーの操作で取りに来る。robots.txt が適用されないことがある', src: SRC.openai },
  { token: 'ClaudeBot', role: 'training', what: 'Anthropicのモデルの学習用', src: SRC.anthropic },
  { token: 'Claude-SearchBot', role: 'search', what: '検索の質の向上用。拒否すると検索での見え方が下がりうる', src: SRC.anthropic },
  { token: 'Claude-User', role: 'user', what: 'Claudeの利用者の質問に答えるための取得。拒否すると取得されない', src: SRC.anthropic },
  { token: 'PerplexityBot', role: 'search', what: 'Perplexityの検索結果に出してリンクするためのもの。学習には使わない', src: SRC.perplexity },
  { token: 'Perplexity-User', role: 'user', what: 'ユーザーの質問に答えるための取得。原則 robots.txt を無視する', src: SRC.perplexity },
  { token: 'Applebot-Extended', role: 'training', what: 'Appleの基盤モデルの学習に使うかの制御(クロール自体は Applebot)', src: SRC.apple },
  { token: 'Bingbot', role: 'search', what: 'Bingの検索のクローラー(公式ヘルプはJSで表示され、本文を静的に確認できず)', src: SRC.bing },
  { token: 'CCBot', role: 'training', what: 'Common Crawl のクローラー(公開データセット)', src: SRC.ccbot },
  { token: 'meta-externalagent', role: 'training', what: 'Metaの基盤モデルの学習・製品改善のための取得', src: SRC.meta },
  { token: 'Bytespider', role: 'unknown', what: '公式の説明文書を確認できず(2026-10-04)', src: '公式文書を確認できず' },
];
const robotsFile = path.join(DIST, 'robots.txt');
const robotsReport = [];
if (!fs.existsSync(robotsFile)) {
  add('info', 'robots.txtが無い(すべて許可として扱われる)', '/robots.txt', '', SRC.rfc9309);
} else {
  // RFC 9309 の読み方:user-agent の行が続いたら同じグループ。名前は大文字小文字を区別しない
  const groups = []; // { agents: [], rules: [{ allow, path }] }
  let cur = null, lastWasAgent = false;
  for (const raw of fs.readFileSync(robotsFile, 'utf8').split('\n')) {
    const line = raw.replace(/#.*/, '').trim();
    const m = line.match(/^([A-Za-z-]+)\s*:\s*(.*)$/);
    if (!m) continue;
    const key = m[1].toLowerCase(), val = m[2].trim();
    if (key === 'user-agent') {
      if (!lastWasAgent) { cur = { agents: [], rules: [] }; groups.push(cur); }
      cur.agents.push(val.toLowerCase());
      lastWasAgent = true;
    } else {
      lastWasAgent = false;
      if (cur && (key === 'allow' || key === 'disallow')) cur.rules.push({ allow: key === 'allow', path: val });
    }
  }
  const ruleMatches = (rule, p) => {
    if (rule.path === '') return false; // 空の disallow は何も止めない
    const re = new RegExp('^' + rule.path.split('*').map((s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*').replace(/\\\$$/, '$'));
    return re.test(p);
  };
  // 最も長い一致を使い、同じ長さなら allow(RFC 9309 2.2.2)
  const allowed = (rules, p) => {
    let best = null;
    for (const r of rules) if (ruleMatches(r, p) && (!best || r.path.length > best.path.length || (r.path.length === best.path.length && r.allow))) best = r;
    return best ? best.allow : true;
  };
  const samplePaths = ['/', ...[...pages.keys()].filter((p) => !pages.get(p).noindex)];
  for (const b of BOTS) {
    const own = groups.filter((g) => g.agents.includes(b.token.toLowerCase()));
    const g = own.length ? own : groups.filter((g) => g.agents.includes('*'));
    const rules = g.flatMap((x) => x.rules);
    const blockedRoot = !allowed(rules, '/');
    const blockedSome = samplePaths.filter((p) => !allowed(rules, p));
    const state = blockedRoot ? '拒否' : blockedSome.length ? `一部拒否(${blockedSome.length}件)` : '許可';
    const via = own.length ? `専用のグループ` : g.length ? '「*」のグループ' : 'グループ無し';
    robotsReport.push({ token: b.token, role: b.role, state, via, what: b.what, src: b.src });
    // 検索・回答用を止めていたら注意(AIの答えに出なくなる)。学習用・ユーザー操作用は情報
    const level = b.role === 'search' && state !== '許可' ? 'warn' : 'info';
    add(level, `${b.token}: ${state}`, '/robots.txt', `${via}で判定|${b.what}`, b.src);
  }
}

// ---- 9. スニペットの制御:AI Overviews / AI Mode にも効く ----
// Google: nosnippet は AI Overviews / AI Mode の直接の入力にも使わせない。max-snippet もその量を制限する。
// data-nosnippet はその部分をスニペットから外す(SRC.gRobotsMeta、SRC.gAiFeatures)
CAT = 'スニペットの制御(AI Overviews / AI Mode)';
for (const [p, v] of pages) {
  if (v.noindex) continue;
  const all = `${v.metaRobots},${v.headerRobots}`.toLowerCase();
  if (/(^|[\s,])nosnippet/.test(all)) add('warn', 'nosnippetが付いている(AI機能に本文が使われない)', p, all.replace(/^,|,$/g, ''), SRC.gRobotsMeta);
  const ms = all.match(/max-snippet\s*:\s*(-?\d+)/);
  if (ms && Number(ms[1]) === 0) add('warn', 'max-snippet:0(nosnippetと同じ扱い)', p, all.replace(/^,|,$/g, ''), SRC.gRobotsMeta);
  else if (ms && Number(ms[1]) > 0) add('info', `max-snippet:${ms[1]}(AI機能に使われる量も制限される)`, p, '', SRC.gRobotsMeta);
  if (v.dataNosnippet) add('info', 'data-nosnippetがある(その部分はスニペット・AI機能に使われない)', p, `${v.dataNosnippet}か所`, SRC.gRobotsMeta);
}

// ---- 10. 静的なHTMLに本文があるか(JavaScriptを実行しないボットもある) ----
// Google: サーバー側の描画や事前描画は、ユーザーとクローラーに速く、すべてのボットがJavaScriptを実行できるわけではない(SRC.gJs)
// しきい値 RULES.mainTextMin は自社の目安
CAT = '静的HTMLの本文';
for (const [p, v] of pages) {
  if (v.noindex) continue;
  if (!v.hasMainOrArticle) add('info', '<main>も<article>も無い(本文エリアを決められない)', p, '', SRC.gJs);
  else if (len(v.mainText) < RULES.mainTextMin) add('warn', `本文エリアの文字が少ない(${RULES.mainTextMin}字未満。JSだけで描画していないか・中身の薄いページでないか)`, p, `${len(v.mainText)}字`, SRC.gJs);
}

// ---- 11. 構造化データと見える内容 ----
CAT = '構造化データと見える内容';
const entities = new Map(); // Person / Organization を @id か名前で1回だけ見る
let faqPages = 0;
for (const [p, v] of pages) {
  for (const node of v.nodes) {
    const t = typesOf(node);
    // FAQPage:質問と答えがページに見えているか(見えない内容をマークアップしない:SRC.gSdPolicies)
    if (t.includes('FAQPage')) {
      faqPages++;
      for (const q of [node.mainEntity].flat().filter(Boolean)) {
        const name = norm(q.name);
        if (name && !v.visibleText.includes(name)) add('error', 'FAQPageの質問がページに見えていない', p, q.name, SRC.gSdPolicies);
        const ans = norm([q.acceptedAnswer].flat()[0]?.text).slice(0, 40); // 答えは先頭40字で照合(リンクなどの書式の差を避ける)
        if (ans && !v.visibleText.includes(ans)) add('warn', 'FAQPageの答えがページの文と一致しない', p, `${q.name}`, SRC.gSdPolicies);
      }
    }
    // Article / BlogPosting / NewsArticle:author(name)・datePublished・dateModified(Google の推奨プロパティ。必須は無い:SRC.gArticle)
    if (t.some((x) => /^(Article|BlogPosting|NewsArticle)$/.test(x))) {
      v.isArticle = true;
      const authors = [node.author].flat().filter(Boolean);
      if (!authors.length) add('warn', 'Articleにauthorが無い', p, t.join(), SRC.gArticle);
      else if (authors.some((a) => typeof a === 'object' && !a.name)) add('warn', 'Articleのauthorにnameが無い', p, t.join(), SRC.gArticle);
      if (!node.datePublished) add('warn', 'ArticleにdatePublishedが無い', p, '', SRC.gArticle);
      if (!node.dateModified) add('info', 'ArticleにdateModifiedが無い', p, '', SRC.gArticle);
      for (const k of ['datePublished', 'dateModified']) {
        if (node[k] && !/T\d{2}:\d{2}.*([+-]\d{2}:?\d{2}|Z)$/.test(node[k])) add('info', `${k}に時刻とタイムゾーンが無い`, p, node[k], SRC.gArticle);
      }
      for (const a of authors) if (typeof a === 'object') entities.set(a['@id'] ?? a.name, { node: a, page: p });
    }
    if (t.includes('Person') || t.includes('Organization')) {
      const key = node['@id'] ?? node.name;
      if (key && !entities.has(key)) entities.set(key, { node, page: p });
    }
  }
}
for (const [key, { node, page }] of entities) {
  // sameAs は「同じ人・組織の別のページ」を示す。Google は著者の見分けに sameAs と url の両方を使える(情報)
  if (!node.sameAs || ![node.sameAs].flat().length) add('info', `${typesOf(node).join() || 'Person/Organization'}にsameAsが無い`, page, key, typesOf(node).includes('Organization') ? SRC.gOrg : SRC.gArticle);
}
if (faqPages) add('info', `FAQPageのあるページ: ${faqPages}(FAQのリッチリザルトは2026-05-07からGoogle検索に表示されない)`, '(サイト全体)', '', SRC.gUpdates);

llms.txt、IndexNow、目安の点検も同じ作りで、根拠のURLはSRCにまとめ、確認日と一緒に出力しています。--json <ファイル名>を付けると、区分・項目・ページ・根拠の入ったJSONも書き出します。

もう一つ、先の記事から変えたところがあります。ファイルがあるかの確認を、名前を1段ずつ照合する形にしました。手元のMacのディスクは大文字小文字を区別しないため、URLの大文字小文字が違っていてもファイルが見つかってしまい、公開先(区別する)では404になるリンクを見落とす作りになっていたからです。

このブログで流した結果:エラー0件、注意76件

2026年10月4日22時48分にビルドしたdistに流した結果です。記事が2本以下のタグの一覧をnoindexにしたあとの状態で、かかった時間は手元のMacで2秒ほどでした。

$ npm run audit
点検したページ: 268(noindex: 27)/記事: 178
サイトマップのURL: 241 / 確かめた内部リンク・画像: 11252
HTMLでないので飛ばした: /feed/
根拠の確認日: 2026-10-04
エラー: 0 / 注意: 76 / 目安: 69 / 情報: 20

■ SEO(基本)(エラー 0・注意 69・目安 0・情報 0)
  [注意] descriptionが短い: 69件  根拠: 公式の根拠なし(自社の目安)
  …(中略)

■ AIクローラー(robots.txt)(エラー 0・注意 0・目安 0・情報 15)
  Googlebot           許可    [search] 「*」のグループ|Google検索のクロール。AI Overviews / AI Mode もこれで制御
  GPTBot              許可    [training] 「*」のグループ|OpenAIの基盤モデルの学習用
  OAI-SearchBot       許可    [search] 「*」のグループ|ChatGPTの検索結果に出すためのもの。拒否するとChatGPTの検索の答えに出ない
  …(15の名前すべてが許可。中略)

■ 静的HTMLの本文(エラー 0・注意 7・目安 0・情報 0)
  [注意] 本文エリアの文字が少ない(200字未満。JSだけで描画していないか・中身の薄いページでないか): 7件
    - /category/fitness/page/5/  47字
    - /category/it/page/8/  45字
    …(中略)

■ 構造化データと見える内容(エラー 0・注意 0・目安 0・情報 1)
  [情報] FAQPageのあるページ: 177(FAQのリッチリザルトは2026-05-07からGoogle検索に表示されない): 1件

■ llms.txt(エラー 0・注意 0・目安 0・情報 3)
  [情報] llms.txt: 自サイトのURL 198件を確認(Google検索は llms.txt を使わない): 1件
  …(中略)

■ IndexNow(エラー 0・注意 0・目安 0・情報 1)
  [情報] IndexNowのキーファイルがある: 1件

■ 目安(公式の根拠なし・自社の目安)(エラー 0・注意 0・目安 69・情報 0)
  [目安] h2の直後が段落でない: 47件
  [目安] 最初の段落の1文目が120字を超える: 22件

スニペットの区分が出力に無いのは、nosnippet・max-snippet・data-nosnippetが1件も無かったからです。robots.txtはUser-agent: *で全体を許可しているだけなので、15の名前すべてが「*」のグループで許可と判定されました。これは、どのAIのクローラーも止めないというこのブログの方針どおりです。

本文が少ないページは、noindexにする前の22時16分のビルドでは23件あり、すべてタグかカテゴリの一覧で、記事の本文ではありませんでした。HTMLの中身を見ると、記事のカードは静的なHTMLに入っていて、JavaScriptだけで描画しているわけではありません。記事が1件しかないタグの一覧のように、そもそも中身が少ないページが引っかかっていました。noindexのページは点検の対象外なので、いま残っている7件は、カテゴリの一覧(ページ送りの最後のページを含む)5件と、記事が3件のタグの一覧2件です。

h2の直後が段落でない47件のうち38件は、記事の最後にある「参照した公式ドキュメント」のような出典の見出しで、すぐ下に出典の箇条書きが続く形でした。残りの9件は、手順やチェックリストの見出しのすぐ下に、番号付きの手順や表が来ているものです。

1文目が120字を超えた22件は、どれも冒頭の「この記事の結論」の1文目が長いものでした。いちばん長いもので212字ありました。

結果をどう扱うか

結果を見て、直したものと、直さないもの、これから直すものに分けました(2026年10月4日時点)。

robots.txtは方針どおりなので変えません。FAQPageの177ページも、記事末に見える形でFAQを載せていて、点検で食い違いが出なかったので、そのままにします。

本文が少ない一覧ページは、記事が2本以下のタグの一覧(22ページ)をnoindexにして、サイトマップから外しました。リンクはたどれるように、followのままです。カテゴリの一覧は、記事が少なくても残しています。以前のブログから引き継いだURLで、外から入ってくる入口になっているからです。残った7件は、このように理由があって残しているページです。

目安の2つは、まだ直していません。h2の直後の47件のうち、出典の見出しの38件は直さず、点検の対象から外します(スクリプトはまだ変えていません)。出典の一覧の前に文章を挟んでも、読む人の役に立たないからです。手順やチェックリストの9件は、見出しのすぐ下に、その節で何をするかを1文で書き足す予定です。1文目が長い22件は、検索で表示が出始めている記事から順に、冒頭を短く言い切る形に書き直す予定です。

わざと壊したコピーで、見落とさないかを確かめた

エラーや注意が0件の区分は、本当に問題が無いのか、見落としているのかが分かりません。そこで、distのコピーに7種類の問題をわざと入れて流しました。

break-llmo.mjsjs
// LLMO・AIO の点検が見落とさないかを確かめるため、コピーした dist をわざと壊す
import fs from 'node:fs';
const D = 'dist-broken';
const edit = (p, fn) => { const f = `${D}${p}index.html`; fs.writeFileSync(f, fn(fs.readFileSync(f, 'utf8'))); };
// 1. robots.txt で検索用(OAI-SearchBot)と学習用(GPTBot)を止め、ClaudeBot はタグの一覧だけ止める
fs.writeFileSync(`${D}/robots.txt`, 'User-agent: *\nAllow: /\n\nUser-agent: GPTBot\nUser-agent: OAI-SearchBot\nDisallow: /\n\nUser-agent: ClaudeBot\nDisallow: /tag/\n');
// 2. max-snippet:0 と data-nosnippet を入れる
edit('/indexnow-cloudflare-pages-deploy/', (h) => h.replace('<head>', '<head><meta name="robots" content="max-snippet:0">').replace('<p>', '<p data-nosnippet>'));
// 3. 本文をJavaScriptで後から入れる形にする(静的なHTMLの<main>を空にする)
edit('/about/', (h) => h.replace(/<main([^>]*)>[\s\S]*<\/main>/, '<main$1><div id="app"></div></main>'));
// 4. FAQPageの質問を、ページに無い文に書き換える(JSON-LDの側だけ)
edit('/supabase-rls-multi-tenant-reservations/', (h) => h.replace(/("@type":"Question","name":")[^"]+/, '$1ページに書いていない質問ですか?'));
// 5. BlogPostingから author と datePublished を消す
edit('/llms-txt-astro-build-auto-generate/', (h) => h.replace(/"datePublished":"[^"]+",/, '').replace(/"author":\{[^{}]*(\{[^{}]*\}[^{}]*)*\},/, ''));
// 6. llms.txt の先頭のH1を消し、存在しないURLを足す
const l = fs.readFileSync(`${D}/llms.txt`, 'utf8');
fs.writeFileSync(`${D}/llms.txt`, l.replace(/^# .*\n/, '') + '\n- [無いページ](https://www.yamayamabloglink.com/no-such-page/)\n');
// 7. IndexNow のキーファイルを消す
for (const f of fs.readdirSync(D)) if (/^[0-9a-f]{32}\.txt$/.test(f)) fs.rmSync(`${D}/${f}`);
console.log('壊したコピーを作りました');

入れた問題は、すべて出ました。OAI-SearchBotの拒否は「注意」、GPTBotの拒否は「情報」、タグの一覧だけを止めたClaudeBotは「一部拒否(10件)」と判定されています(noindexのページは数えないので、インデックス可のタグの一覧の数です)。max-snippet:0は「注意」、data-nosnippetは「情報」で出ました。<main>を空にしたページは「本文エリアの文字が少ない(0字)」、ページに無い質問に書き換えたFAQPageは「エラー」、authorとdatePublishedを消した記事はそれぞれ「注意」です。llms.txtは、H1が無いことと、存在しないURLの両方が「注意」で出て、キーファイルを消すと「キーファイルが一番上に無い」に変わりました。

$ node break-llmo.mjs && node site-audit.mjs ./dist-broken https://www.yamayamabloglink.com
壊したコピーを作りました
点検したページ: 268(noindex: 27)/記事: 178
サイトマップのURL: 241 / 確かめた内部リンク・画像: 11251
HTMLでないので飛ばした: /feed/
根拠の確認日: 2026-10-04
エラー: 2 / 注意: 83 / 目安: 69 / 情報: 20
  GPTBot              拒否    [training] 専用のグループ|OpenAIの基盤モデルの学習用
  OAI-SearchBot       拒否    [search] 専用のグループ|ChatGPTの検索結果に出すためのもの。拒否するとChatGPTの検索の答えに出ない
  ClaudeBot           一部拒否(10件)  [training] 専用のグループ|Anthropicのモデルの学習用
  [注意] max-snippet:0(nosnippetと同じ扱い): 1件
  [情報] data-nosnippetがある(その部分はスニペット・AI機能に使われない): 1件
    - /about/  0字
  [エラー] FAQPageの質問がページに見えていない: 1件
  [注意] Articleにauthorが無い: 1件
  [注意] ArticleにdatePublishedが無い: 1件
  [注意] llms.txtのURLがdistに無い: 1件
  [注意] llms.txtの先頭がH1(# サイト名)でない: 1件
  [情報] IndexNowのキーファイルが一番上に無い(keyLocationで別の場所を指定している場合を除く): 1件

エラーの2件目は、<main>を空にしたことでh1も消え、SEOの区分の「h1が無い」が出たものです。終了コードは1になり、ビルドの流れに入れればそこで止まります。

ファイルを読むだけでは分からないこと

このスクリプトで分かるのは、AI検索に出る前提を自分で壊していないかまでです。次のことは分かりません。

一つ目は、AIに実際に引用・紹介されているかです。Googleによると、AI OverviewsとAIモードに出たページの数字は、Search Consoleの検索パフォーマンスの「ウェブ」に含めて集計されます。2026年8月31日からは、すべてのサイトで「生成 AI パフォーマンス」レポートが使えるようになり、AI OverviewsとAIモードでの表示回数を分けて見られます。ChatGPTやPerplexityで紹介されているかは、実際に質問して確かめるほかありません。

二つ目は、CDNやWAFでの遮断です。robots.txtですべて許可していても、CDNのボット対策の設定で止まっていれば読まれません。このブログはCloudflareを通しているので、管理画面の設定を別に確かめています(手順はAIクローラーのrobots.txt設定例の記事にあります)。

三つ目は、インデックスされているかと、JavaScriptで描画したあとの姿です。スクリプトが見ているのはビルドが書き出したHTMLだけなので、Googleが実際にどう受け取ったかは、Search ConsoleのURL検査で確かめます。サーバーが_headers以外で付けるヘッダーも、ここでは見えません。

最後に、各社の説明そのものが変わることです。根拠にした文書は、どれも2026年10月4日に読んだ内容で、AI検索まわりは書き換わりが早いので、半年に1回は読み直すつもりです。

AI Overviewsの仕組みと、サイト側で自分のページを外す設定は、会社のコラムAIによる概要(AI Overview)とは。答えの作られ方と、消せるのかに、利用する人とサイトを運営する人の両方に向けてまとめています。株式会社bundlyzeでは自社サイトでもLLMOを実践していて、検索とAIの両方に向けたサイトの整え方をSEO・LLMO対策のページで紹介しています。

動作確認した環境

確認日は2026年10月4日、使ったのは手元のMacです。

項目 バージョン・内容
OS macOS 26(Darwin 25.6.0)
Node.js 22.23.2
cheerio 1.2.0
点検した相手 このブログを2026年10月4日22時48分にビルドしたdist(Astro 7.3.5。404.htmlとRSSのfeed/index.htmlを除く268ページ)
確かめたこと 実際のdistでの結果、7種類の問題を入れたコピーでの検出、大文字小文字だけが違うURLを見落とさないこと、1回にかかる時間(約2秒)

CIでの実行、Windowsでのパスの扱い、Astro以外の書き出しの形は試していません。記事本文の入れ物は、このブログの作り(articleの中の.prose)に合わせているので、ほかのサイトで使うときは、その部分の指定を変えてください。

参照した公式ドキュメント(確認日:2026年10月4日)

よくある質問

robots.txtで学習用のAIクローラーを止めると、AIの答えに出なくなりますか?

各社の説明では、学習用と検索用は別の名前で扱われています。OpenAIは、GPTBotを止めるのは学習に使わないでほしいという意思表示で、ChatGPTの検索の答えに出すかはOAI-SearchBotで決まるとしています。Googleも、Google-Extendedは検索への掲載や順位に影響しないと書いています。止めると答えに出なくなる、または出にくくなるのは、検索用の名前(OAI-SearchBot、Claude-SearchBot、PerplexityBot、Googlebotなど)のほうです。

llms.txtを置けば、AI OverviewsやAIモードに出やすくなりますか?

Googleは、Google検索はllms.txtを使っておらず、置いても検索での見え方や順位に良くも悪くも影響しないと書いています。llms.txtはllmstxt.orgの提案で、ほかのサービスのために置くのは構わない、という扱いです。このスクリプトでは、置いているなら中身のURLが正しいかだけを見ています。

FAQのリッチリザルトは終わったのに、FAQPageの構造化データを点検する意味はありますか?

Googleは、FAQのリッチリザルトを2026年5月7日から検索結果に表示しなくなりました。一方で、ページに見えていない内容をマークアップしないという決まりは、構造化データ全般の方針です。このブログは記事末にFAQを見える形で載せ、同じ内容をFAQPageにしているので、両者がずれていないかだけを点検しています。

このスクリプトで、AIに引用されているかどうかは分かりますか?

分かりません。ビルド後のファイルを読むだけなので、分かるのは引用される前提を自分で壊していないかまでです。Googleによると、AI OverviewsやAIモードに出たページの数字は、Search Consoleの検索パフォーマンスの「ウェブ」に含まれ、2026年8月31日からは「生成 AI パフォーマンス」レポートで分けて見ることもできます。ChatGPTやPerplexityでの紹介は、実際に質問して確かめるしかありません。