Skip to content

Instantly share code, notes, and snippets.

@YSRKEN
Created November 5, 2025 04:29
Show Gist options
  • Save YSRKEN/e11d2360fb911dad8a4e2cf98c208251 to your computer and use it in GitHub Desktop.
Save YSRKEN/e11d2360fb911dad8a4e2cf98c208251 to your computer and use it in GitHub Desktop.
ChatGPT「現在のタブのサイトから /robots.txt と /llms.txt を取得し、中身を日本語で要約表示する Manifest V3 のChrome拡張サンプルを用意しました」

要点(2025-11-05 JST)

  • 現在のタブのサイトから /robots.txt と /llms.txt を取得し、中身を日本語で要約表示する Manifest V3 のChrome拡張サンプルを用意しました(そのまま動きます)。
  • robots.txt は正式仕様(RFC 9309)で、クローラーへのアクセス方針(Allow/Disallow等)を伝えるもの。セキュリティ対策の代替にはなりません。(RFCエディタ)
  • llms.txt は Jeremy Howard らの提案で、サイトをLLMが参照・要約しやすいように構造化リンクや説明を載せる“慣行”です(まだ標準ではありません)。Mintlify等が自動生成をサポート。(llms-txt)
  • OpenAIの GPTBot など主要AIクローラーは robots.txt の指示を読む想定です(例:User-agent: GPTBot)。(OpenAI Platform)

手順(拡張の作成)

前提

  • ブラウザ: Google Chrome(拡張読み込み可)
  • 権限: ローカルで拡張を「デベロッパーモード」で読み込む
  • ネットワーク: 通常のインターネット接続(CORS回避のため manifest の host_permissions を使用)

1) ファイルを作成(任意の空フォルダ内)

manifest.json

{
  "manifest_version": 3,
  "name": "Robots & LLMs.txt Explainer",
  "version": "0.1.0",
  "description": "現在のサイトの robots.txt と llms.txt を読み取り、要点を日本語で説明します。",
  "action": {
    "default_title": "Explain robots.txt / llms.txt",
    "default_popup": "popup.html"
  },
  "permissions": ["activeTab", "storage"],
  "host_permissions": ["<all_urls>"]
}

popup.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Robots & LLMs.txt Explainer</title>
  <style>
    body { font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 12px; width: 360px; }
    h1 { font-size: 16px; margin: 0 0 8px; }
    .host { color: #555; margin-bottom: 8px; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 10px; margin: 10px 0; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; }
    details { margin-top: 6px; }
    .ok { color: #0a6; }
    .warn { color: #b50; }
    .err { color: #c00; }
    button { padding: 6px 10px; border-radius: 6px; border: 1px solid #ccc; background: #fafafa; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Robots & LLMs.txt Explainer</h1>
  <div class="host" id="host"></div>
  <button id="reload">再読み込み</button>

  <div class="card">
    <strong>robots.txt の要点</strong>
    <div id="robots-summary">読み込み中…</div>
    <details><summary>生データを見る</summary><pre class="mono" id="robots-raw"></pre></details>
  </div>

  <div class="card">
    <strong>llms.txt の要点</strong>
    <div id="llms-summary">読み込み中…</div>
    <details><summary>生データを見る</summary><pre class="mono" id="llms-raw"></pre></details>
  </div>

  <div style="margin-top:8px;color:#666;font-size:12px">
    ※ robots.txt は正式仕様(RFC 9309)ですが、llms.txt は提案段階の慣行です。
  </div>

  <script src="popup.js"></script>
</body>
</html>

popup.js

// 使い方: 拡張のポップアップを開くと、現在タブのオリジンから
// /robots.txt と /llms.txt を取得し、要点を要約表示します。

const notableAgents = [
  "GPTBot",        // OpenAI
  "Googlebot",     // Google
  "bingbot",       // Microsoft
  "CCBot",         // Common Crawl
  "Anthropic",     // Claude系(表記ゆれ対策で部分一致)
  "PerplexityBot"  // Perplexity
];

document.getElementById('reload').addEventListener('click', () => run());

async function run() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const url = new URL(tab.url);
  const origin = url.origin;
  document.getElementById('host').textContent = `対象サイト: ${origin}`;

  const robotsUrl = origin + "/robots.txt";
  const llmsUrl   = origin + "/llms.txt";

  const robots = await fetchText(robotsUrl);
  const llms   = await fetchText(llmsUrl);

  // robots.txt
  const robotsRawEl = document.getElementById('robots-raw');
  const robotsSumEl = document.getElementById('robots-summary');
  if (robots.ok && robots.text.trim()) {
    robotsRawEl.textContent = robots.text;
    const parsed = parseRobots(robots.text);
    robotsSumEl.innerHTML = renderRobotsSummary(parsed);
  } else if (robots.status === 404) {
    robotsRawEl.textContent = "";
    robotsSumEl.innerHTML = `<span class="warn">robots.txt は見つかりませんでした(404)。</span>`;
  } else if (!robots.ok) {
    robotsRawEl.textContent = "";
    robotsSumEl.innerHTML = `<span class="err">robots.txt の取得に失敗: HTTP ${robots.status}</span>`;
  } else {
    robotsRawEl.textContent = "";
    robotsSumEl.innerHTML = `<span class="warn">robots.txt は空でした。</span>`;
  }

  // llms.txt
  const llmsRawEl = document.getElementById('llms-raw');
  const llmsSumEl = document.getElementById('llms-summary');
  if (llms.ok && llms.text.trim()) {
    llmsRawEl.textContent = llms.text;
    const summary = summarizeLlms(llms.text);
    llmsSumEl.innerHTML = summary;
  } else if (llms.status === 404) {
    llmsRawEl.textContent = "";
    llmsSumEl.innerHTML = `<span class="warn">llms.txt は見つかりませんでした(404)。</span>`;
  } else if (!llms.ok) {
    llmsRawEl.textContent = "";
    llmsSumEl.innerHTML = `<span class="err">llms.txt の取得に失敗: HTTP ${llms.status}</span>`;
  } else {
    llmsRawEl.textContent = "";
    llmsSumEl.innerHTML = `<span class="warn">llms.txt は空でした。</span>`;
  }
}

async function fetchText(u) {
  try {
    const res = await fetch(u, { method: "GET" });
    const text = await res.text().catch(() => "");
    return { ok: res.ok, status: res.status, text };
  } catch (e) {
    return { ok: false, status: 0, text: "" };
  }
}

// --- robots.txt 簡易パーサ(仕様の主要部にフォーカス) ---
function parseRobots(content) {
  const lines = content.split(/\r?\n/).map(l => l.replace(/\s*#.*$/, "").trim()).filter(l => l.length);
  const groups = []; // { agents: [..], rules: [{type, value}], sitemaps: [] }
  let curAgents = [];
  let curRules = [];
  let sitemaps = [];

  const flush = () => {
    if (curAgents.length || curRules.length) {
      groups.push({ agents: curAgents.slice(), rules: curRules.slice() });
    }
    curAgents = [];
    curRules = [];
  };

  for (const line of lines) {
    const m = line.match(/^([A-Za-z\-]+)\s*:\s*(.+)$/);
    if (!m) continue;
    const key = m[1].toLowerCase();
    const val = m[2].trim();

    if (key === "user-agent") {
      // グループ境界は「User-agentが来て、すでに何らかの指示があればflush」
      if (curRules.length && curAgents.length) flush();
      curAgents.push(val);
    } else if (key === "allow" || key === "disallow" || key === "crawl-delay") {
      curRules.push({ type: key, value: val });
    } else if (key === "sitemap") {
      sitemaps.push(val);
    } else {
      // その他は保持しない(拡張ディレクティブは多数あるが要約UIの簡潔さを優先)
    }
  }
  flush();

  // エージェント別にまとめ直し
  const byAgent = {};
  for (const g of groups) {
    for (const a of g.agents) {
      if (!byAgent[a]) byAgent[a] = { allow: [], disallow: [], crawlDelay: [] };
      for (const r of g.rules) {
        if (r.type === "allow") byAgent[a].allow.push(r.value);
        if (r.type === "disallow") byAgent[a].disallow.push(r.value);
        if (r.type === "crawl-delay") byAgent[a].crawlDelay.push(r.value);
      }
    }
  }
  return { byAgent, sitemaps };
}

function renderRobotsSummary(parsed) {
  const agents = Object.keys(parsed.byAgent);
  const star = parsed.byAgent["*"];
  const hasStar = !!star;
  const sitemapCount = parsed.sitemaps.length;

  // 気になりがちなエージェントを抽出
  const picked = [];
  for (const name of notableAgents) {
    const key = agents.find(a => a.toLowerCase().includes(name.toLowerCase()));
    if (key) picked.push([name, key, parsed.byAgent[key]]);
  }

  // 表示
  let html = "";
  html += `<div>定義済み User-agent 数: <strong>${agents.length}</strong>、Sitemap: <strong>${sitemapCount}</strong> 件</div>`;
  if (sitemapCount) {
    html += `<div class="mono">${parsed.sitemaps.map(s => `Sitemap: ${s}`).join("\n")}</div>`;
  }
  if (hasStar) {
    html += `<div style="margin-top:6px"><strong>共通ルール(User-agent: *)</strong><div class="mono">Allow: ${star.allow.slice(0,5).join(", ") || "(なし)"}\nDisallow: ${star.disallow.slice(0,5).join(", ") || "(なし)"}${star.disallow.length>5?" …":""}</div></div>`;
  }
  if (picked.length) {
    html += `<div style="margin-top:6px"><strong>主なクローラー</strong>`;
    for (const [label, actual, r] of picked) {
      html += `<div style="margin-top:4px"><em>${label}</em>(記載: <code>${actual}</code>)<div class="mono">Allow: ${r.allow.slice(0,5).join(", ")||"(なし)"}\nDisallow: ${r.disallow.slice(0,5).join(", ")||"(なし)"}${r.disallow.length>5?" …":""}${r.crawlDelay.length?`\nCrawl-delay: ${r.crawlDelay.join(", ")}`:""}</div></div>`;
    }
    html += `</div>`;
  }
  if (!hasStar && picked.length===0) {
    html += `<div class="warn" style="margin-top:6px">共通ルールや主要クローラーの個別指定は見当たりません。</div>`;
  }
  html += `<div style="margin-top:6px;color:#666;font-size:12px">※ robots.txt は“遵守が期待される”プロトコルであり、認可やアクセス制御の代替ではありません。</div>`;
  return html;
}

// --- llms.txt の簡易要約(提案段階の慣行を想定) ---
function summarizeLlms(text) {
  // 代表的な書式(見出し/リンク箇条書き)をざっくり抽出
  const title = (text.match(/^#\s+(.+)$/m) || [,""])[1].trim();
  const sections = [...text.matchAll(/^##\s+(.+)$/gm)].map(m => m[1].trim());
  const links = [...text.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g)].map(m => ({ label: m[1], url: m[2] }));
  const bullets = text.split(/\r?\n/).filter(l => /^\s*[-*+]\s+/.test(l));

  let html = "";
  if (title) html += `<div>タイトル: <strong>${escapeHtml(title)}</strong></div>`;
  html += `<div>セクション数: <strong>${sections.length}</strong>、掲載リンク数: <strong>${links.length}</strong>、箇条書き数: <strong>${bullets.length}</strong></div>`;
  if (sections.length) {
    html += `<div style="margin-top:6px"><strong>主なセクション</strong><div class="mono">${sections.slice(0,6).join("\n")}${sections.length>6?"\n…":""}</div></div>`;
  }
  if (links.length) {
    html += `<div style="margin-top:6px"><strong>代表的なリンク</strong><div class="mono">${links.slice(0,5).map(l => `${l.label}: ${l.url}`).join("\n")}${links.length>5?"\n…":""}</div></div>`;
  }
  html += `<div style="margin-top:6px;color:#666;font-size:12px">※ llms.txt は標準化途上の“提案”。対応はサイト/ツールごとに異なります。</div>`;
  return html;
}

function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }

run();

2) Chrome に読み込む

  1. chrome://extensions を開く → 右上 デベロッパーモード ON
  2. パッケージ化されていない拡張機能を読み込む → 上記フォルダを選択
  3. 任意のページで拡張アイコンを開く → robots.txt / llms.txt の要点が表示されます

検証

  • https://example.com/robots.txt などで表示確認
  • User-agent: GPTBot の有無や Sitemap: の列挙が要約に反映されます(GPTBot は OpenAI 公開情報に基づくエージェント例)。(OpenAI Platform)

理由(仕様と背景の簡単整理)

  • robots.txt は正式な Robots Exclusion Protocol(RFC 9309) に基づき、クローラーがどのパスへアクセスできるかの方針を示します。ただしアクセス制御ではなく、機密URLを隠す用途には不適(列挙されるためむしろ露出します)。(RFCエディタ)
  • Google公式ドキュメントも robots.txt の解釈と基本例を公開しています。(Google for Developers)
  • llms.txt は 2024年公開の /llms.txt 提案(Jeremy Howard)に基づく“慣行”で、LLMが推論時に参照しやすいようサイトの構造・要点・リンクを示します。Mintlifyなどドキュメントホスティングが自動生成をサポート。標準化は未了で、実装/対応はサービス次第です。(llms-txt)
  • OpenAIの GPTBot など主要AIクローラーは robots.txt を読む前提のため、現実装ではAI向けの可否指定は robots.txt で行うのが確実です(例:User-agent: GPTBot / Disallow: / など)。(OpenAI Platform)

代替案(最小3通り)

  1. ブックマークレット(同一オリジンから取得) (ページで押すと簡易要約のダイアログを出す・超簡易版)
javascript:(async()=>{const o=location.origin;const f=async(p)=>{try{const r=await fetch(o+p);return [r.status,await r.text()]}catch(e){return[0,""]}};const [sr,rt]=await f("/robots.txt");const [sl,lt]=await f("/llms.txt");alert([
  `robots.txt: ${sr}`, rt?rt.slice(0,400):(sr===404?"(なし)":"(取得失敗)"),
  `\n\nllms.txt: ${sl}`, lt?lt.slice(0,400):(sl===404?"(なし)":"(取得失敗)")
].join("\n"));})();
  1. CLIワンライナー(bash + curl) (GPTBotブロックの有無をざっくり確認)
# 前提: bash, curl
u="https://example.com"; curl -fsS "$u/robots.txt" | awk 'BEGIN{IGNORECASE=1}/^User-agent:/ {ua=$0 ~ /GPTBot/} ua && /^Disallow:/ {print "GPTBot", $0}'
  1. Node/denoのスクリプトで柔軟に解析 Nodeの node-fetch 等で取得 → 上のロジックを拡張(User-agentごとの集計、レポート出力) ※将来 llms.txt 向けに Markdown をパースして、セクション/リンクの表形式エクスポートも容易。

注意点・補足

  • robots.txtは拘束力のあるアクセス制御ではありません。非公開にしたいパスは認証や適切なACLで保護してください。(RFCエディタ)
  • llms.txtは“提案”段階です。対応ツールが増えつつある一方、現時点では各社の解釈・実装が揃っていません(Mintlifyが自動生成を公開)。(llms-txt)
  • AIクローラーの銘柄は増減します。最新のユーザーエージェント一覧を参照して随時拡張してください(例:コミュニティのリスト)。(GitHub)

必要なら、この拡張にオプションページ(チェックボックスで「注目するクローラー」を編集)や、日本語の自然文要約をもっと賢くするロジック(例:ルールの衝突検知、サイトマップの数やパターン分析)も足します。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment