000%

更新

NOTES · SEO / GEO / AEO10 · 2026.10

代表URL・クロール指示・サイトマップを構成する

検索システムに渡すURLをページ・リンク・サイトマップでそろえる。公開パスのクロール指示を定め、改修前のURLから新しい文書に到達できるようにする。

§ 01SEO / GEO / AEO

重複URLと言語別URLを整理する

新しい運営案内を公開しても、そのURLへのリンクがなければクローラーがURLを発見する経路は限られる。URLを知っていてもrobotsやサーバーがリクエストを遮断すると本文を取得できず、同じ記事が複数のURLにあれば代表URLを選ぶ必要がある。内部リンク・サイトマップ、クロール指示、canonicalはそれぞれこの問題を扱う。公開文書が発見・処理される順に設定を読む。

同じ記事が複数のURLで開くなら、運用する代表URLを先に決める。httpとhttps、wwwの有無、末尾スラッシュ、旧slugの関係を一覧にする。使わなくなる同等のURLは、サーバーやホスティングの恒久リダイレクトで代表URLにつなぐ。追跡用クエリだけが異なるURLは、本文が同じなら同じcanonicalを使える。フィルターが実際に異なるコンテンツを提供するなら、まず重複かどうかを判断する。

ページ分割の2ページ目は1ページ目と内容が異なるため、すべてのページの代表を無条件に最初のページにしない。次のページに進む実際のa hrefリンクを用意する。多言語ページがある場合は、各言語の自己参照canonicalと相互のhreflang関係を作り、存在しない翻訳URLは出力しない。

サイトマップ・内部リンク・canonical・リダイレクトが異なる代表URLを示さないようにそろえる。Googleの正規URLガイドに従って構成し、デプロイ後に検索システムが実際に選んだ代表URLをURL検査で確認する。

§ 02SEO / GEO / AEO

改修前後のURLを1行ずつ対応させる

改修前に既存URLを一覧として保存し、それぞれを維持・移転・過去記録の保存・終了のいずれかに指定する。同じ内容のURL移動は301/308で新しいURLにつなぐ。現在の案内と過去記録は適用期間と文書の目的で区別する。過去記録には当時の基準時点と現在の案内へのリンクを付け、別のURLで維持できる。

例えば/notice/old-guideを/guides/applyに移したら、リダイレクト、メニュー・本文のリンク、canonical、サイトマップをまとめて変更する。旧案内を/archive/2024/applyに保存する場合、その本文を現在の案内で上書きしない。アーカイブを検索から除外するかは公開方針で決め、古いという理由だけで一律にnoindexを付けない。

ルート対応表には旧URL・新URL・期待するステータスコード・代表URL・サイトマップへの掲載・流入元リンクを記す。旧URLが別の旧URLを経由して何度も転送されたり、ループしたりしないか確認する。運用ドメインが変わる場合は、絶対URLを生成する環境変数、共有画像のURL、検索ツールのプロパティと所有権確認も更新する。GoogleのURL変更を伴うサイト移転ガイドを参照する。

旧URLは検索結果・外部記事・保存したリンクに残り続けることがある。同じ記事が新しいURLに移った場合に恒久リダイレクトを提供すると、そのURLを訪れた人とクローラーを新しい文書へ案内できる。過去の運用基準を保存した記録なら、当時の質問に答える資料なので固有のURLと期間を維持する。古いURLをすべてホームに送ると、訪問者が探していたものにつなげにくい。

§ 03SEO / GEO / AEO

robots.txtとレスポンスヘッダーを変更する

Next.jsではapp/robots.tsとpublic/robots.txtのどちらが実際の提供元かを確認する。他のサーバーではWebルートのrobots.txtとCDN・プロキシの遮断ルールを見る。公開パスがDisallowに含まれる場合は公開方針に合わせて範囲を狭める。HTMLのmeta robotsとHTTPのX-Robots-Tagに残るnoindexも調べる。robots.txtに書いたnoindexはGoogleのインデックス除外指示としてサポートされない。

検索用ボットと学習用ボットは目的を分けて判断する。以下は検索クロールを許可し、OpenAIの学習用クロールを制限すると決めたサイトの例だ。学習用クロールも許可するサイトでは、GPTBotのDisallowルールをその方針に合わせて変更する。

User-agent: * Allow: / Disallow: /admin$ Disallow: /admin? Disallow: /admin/ Disallow: /preview$ Disallow: /preview? Disallow: /preview/ User-agent: OAI-SearchBot Allow: / Disallow: /admin$ Disallow: /admin? Disallow: /admin/ Disallow: /preview$ Disallow: /preview? Disallow: /preview/ User-agent: GPTBot Disallow: / Sitemap: https://example.com/sitemap.xml

特定ボットに適用するグループには、そのボットの除外パスも明記する。一般グループのルールを統合しない処理でも除外条件が伝わるように、例の検索ボットグループではパスを繰り返している。プレビューパスは実際の認証で保護し、公開本文に必要なCSS・JavaScript・画像・公開APIが広い遮断ルールに含まれないか確認する。WAFがボットにログイン・チャレンジ・403を返す場合、robotsの許可だけでは解決しない。

Googleのnoindex指示はページのレスポンスに入る。クローラーがそのレスポンスを受け取れて初めて指示を読める。OAI-SearchBotは検索用資料、GPTBotはモデル学習用資料を収集するため、目的ごとに許可パスを決める。Google noindex · OpenAIのボット

/admin/だけでは/adminと/admin?mode=editに一致しない。上のルールはパスの末尾を示す$で正確なルートを、?でルートのクエリを、/で下位パスを指定する。/administratorはこの3つのルールに含まれない。Googleがサポートする*・$とパス比較ルールに基づく例だ。他のクローラーでは提供元の対応範囲を確認する。robotsのパスルール

§ 04SEO / GEO / AEO

公開データからサイトマップを生成する

サイトマップは別の手書き一覧より、公開クエリの結果から作るほうが漏れを減らせる。Next.jsのapp/sitemap.tsではURLと更新日を返し、他のスタックでは同じデータからXMLを生成する。下のlistPublicGuidesは公開条件を適用した全件取得だ。DB取得がページ単位なら、最後のページまで反復する必要がある。

次のブロックは変換の原理を示す関数部分だ。実行用のapp/sitemap.ts全体とimportは次の節にある。

export default async function sitemap() { const guides = await listPublicGuides() return guides.map(g => ({ url: g.info.canonical, ...(g.info.dateModified ? { lastModified: g.info.dateModified } : {}) })) }

guidesは公開記事全件の配列で、mapは記事ごとにサイトマップ項目を作る。urlには検証済みのinfo.canonicalを入れ、dateModifiedがある場合だけlastModifiedを追加する。...は条件に応じて作ったプロパティをオブジェクトに結合する構文だ。日付文字列をそのまま使い、日付しか分からない更新日に任意の午前0時を付けない。

lastModifiedには本文や重要なリンクが実際に変わった日付を入れる。デプロイ時刻やリクエストごとの現在時刻で埋めない。信頼できる更新日がなければ省略する。サイトマップのURLは正常に応答する公開の代表URLでなければならず、撤回・エラー・リダイレクトのURLは除外する。サイトマップ作成ガイドに従って生成ファイルを提供し、検索ツールで送信・処理状況を確認する。

RSS・Atomは新しい記事の購読や外部連携に使う文書形式だ。フィードは、その形式でタイトル・リンク・日付をまとめて提供するURLを指す。運用するならサイトマップと同じ公開条件を適用し、タイトル・代表リンク・日付・安定した項目IDを出力する。購読ツールや連携サービスが使うURLを決め、公開・更新・撤回時にはフィードも更新する。

公開クエリからサイトマップを作る理由は、CMSの記事が増えると発見用URLも一緒に増えるようにするためだ。例えば詳細記事が120件あるのに最初の一覧20件だけをサイトマップに入れると、残り100件の発見は他のリンク経路に依存する。全件取得と正常URLの検査をつなげれば、この漏れを見つけられる。lastmodは変更された文書を知らせる資料なので、実際の重要な変更時点に合わせる。

§ 05SEO / GEO / AEO

例のrobotsとサイトマップを完成させる

app/robots.tsは公開パスのクロールを案内し、管理者・プレビューパスを除外する。認証は該当サーバー機能で別途適用する。app/sitemap.tsには公開クエリの結果と実在する保存ページを含める。

import type { MetadataRoute } from 'next' import { SITE_ORIGIN } from '../lib/site' export default function robots(): MetadataRoute.Robots { return { rules: [{ userAgent: '*', allow: '/', disallow: ['/admin$', '/admin?', '/admin/', '/preview$', '/preview?', '/preview/'] }], sitemap: new URL('/sitemap.xml', SITE_ORIGIN).href } }

import type { MetadataRoute } from 'next' import { listPublicGuides } from '../lib/guides' import { SITE_ORIGIN } from '../lib/site' export const dynamic = 'force-dynamic' export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const guides = await listPublicGuides() return [ { url: SITE_ORIGIN }, { url: new URL('/policies/opening-hours', SITE_ORIGIN).href }, { url: new URL('/archive/opening-2025', SITE_ORIGIN).href }, ...guides.map(g => ({ url: g.info.canonical, ...(g.info.dateModified ? { lastModified: g.info.dateModified } : {}) })), ] }

サイトマップに下書き・存在しないURL・リダイレクトURLが含まれず、各URLが正常な公開文書を返すことを確認する。URL数が増えたらファイルを分割し、サイトマップインデックスでつなぐ。Googleの一般的なサイトマップ1ファイルの上限はURL 50,000件と非圧縮50MBだ。サイトマップ作成ガイドに従ってファイルサイズと全件一覧の漏れを確認する。

最初のコードはapp/robots.ts、2つ目はapp/sitemap.tsだ。robots関数はrulesをrobots.txt形式に、sitemap関数は公開URL配列をXMLに変換するようNext.jsへ渡す。ホーム・過去記録・根拠文書と現在の公開記事を含み、下書きは除く。curl http://localhost:3027/robots.txtとcurl http://localhost:3027/sitemap.xmlで実際の生成結果を読む。

現在の例は一般的なクロール許可方針を使っている。前節のGPTBot制限例を採用するなら、このrobots関数のrulesにも同じ方針を反映する。public/robots.txtとapp/robots.tsを同時に作り、どちらが応答するか不明な状態にしない。

§ 06SEO / GEO / AEO

クロール可能性とインデックス登録可能性を別々に検査する

crawlabilityはクローラーが資料を取得できる条件、indexabilityは取得した資料がインデックス登録の対象になれる条件を指す。HTTP 200でrobotsが許可していても、noindex、代表文書の選択、コンテンツの判断によって検索に含まれない場合がある。クロールを遮断すると、クローラーがページ内のnoindexを読めなくなることがある。

公開ファイルを検索だけから除外したい場合は、アクセス可能なレスポンスにmeta robotsまたはX-Robots-Tagのnoindexを設定する。非公開資料は認証と権限で保護する。許可していないユーザーのリクエストが実際に遮断されることを確認する。robots.txtは公開された案内ファイルなので、秘密のパスを隠す手段として使わない。

クロールバジェットは、検索エンジンがサイトをクロールする資源と需要の制約に関わる概念だ。非常に大きなサイトや変化の速い資料では、重複・無限URL・サーバー応答の問題がクロール効率に影響することがある。小さな案内サイトでは、まず重要文書の発見リンク・エラー・本文・インデックス除外の原因を確認する。大規模サイトのクロールガイド

地域の事業所は支店ごとの住所・営業時間・連絡先を提供する。商品には価格・在庫・購入条件を、動画には再生URL・サムネイル・長さを提供する。翻訳ページにはその言語の本文と、相互に対応するhreflangのURLが必要だ。コンテンツの種類によって追加する情報が変わる。

§ 07SEO / GEO / AEO

生成型検索への参加条件と本文の使用範囲を設定する

Googleの生成型検索に参加するページは、インデックスに登録され、検索結果でスニペットを表示できる必要がある。Search ConsoleのSettings → Search generative AIで、対象プロパティと上位プロパティの適用値も確認する。Includeが初期値で、下位プロパティは親の値を継承できる。検索には表示されるのに生成型機能に参加できない場合は、インデックス状況、スニペット指示、この設定の順に照合する。参加条件 · Search generative AIの設定

<!-- ページ全体のスニペットを除外する選択肢:サンプルサイトの初期設定ではない --> <meta name="robots" content="nosnippet"> <!-- 別の選択肢:スニペットの最大長を制限する --> <meta name="robots" content="max-snippet:160"> <!-- ページの一部をスニペットから除外する選択肢 --> <p>日曜日10–17時、第1日曜日は休館。</p> <div data-nosnippet>画面操作のための補足文</div>

3つのコードは別々の選択肢だ。nosnippetはテキストスニペットを遮断し、Google AI Overviews・AI Modeの直接入力としてコンテンツを使うことも制限する。max-snippet:0は同じ除外を意味し、正の値はスニペットと直接入力の長さに影響する。data-nosnippetはspan・div・sectionに付け、その部分を除外する。重要な休館条件をその中に入れると、必要な答えの根拠が抜けることがある。別途の許可・契約や構造化データなどには適用の例外があるため、文書で指示の範囲を確認する。Googleのスニペット指示

GooglebotはGoogle検索のクロールを担当する。Google-Extendedは、一部のGeminiモデル学習とGoogle検索外で指定されたグラウンディング利用を管理するrobotsのプロダクトトークンだ。このトークンだけでGoogle Searchの順位や生成型検索への参加を制御することはできない。Search generative AIの除外設定は検索の指定された生成型機能に適用され、他の検索順位やモデル学習を制御しない。Googleクローラーの範囲 · 設定の適用範囲

OpenAIのOAI-SearchBotとGPTBotも、検索と学習という別々の用途を持つ。公開パスを許可した後は実際のレスポンスのrobotsヘッダーと本文を読み、サーバーログのリクエスト結果を記録する。検索結果への参加と学習の許可を1つの「AI許可」スイッチにまとめると、どの目的を制限したか説明しにくくなる。

§ 08SEO / GEO / AEO

最近の記事をRSSで提供し、更新・撤回に合わせて更新する

RSSは購読ツールが同じURLを定期的に読み、新しい記事を見つける形式だ。ホームページを毎回訪れない読者や他のサービスにニュースを届けられる。サイトマップはサイトの公開URLを検索ツールに知らせ、この実習のRSSは最新投稿20件のタイトル・代表URL・要約を送る。GoogleはRSS・Atomもサイトマップとして受け付けるが、最近の記事だけを含むフィードは全URL一覧の代わりにはならない。サイトマップ形式のガイド

下のfeed.mjsをidentity-time.mjsと同じフォルダーに保存する。renderFeedは元データの全行から公開文書を選び、公開日順に20件を選択する。guidには文書ID、linkには現在の代表URLを入れる。タイトルやslugを変えても同じ記事であるという関係を維持する。RSS 2.0仕様

import { describeDocument, instant } from './identity-time.mjs'; export function escapeXml(value) { // XML 1.0で許可されない文字と単独のサロゲートを拒否する。 for (const char of String(value)) { const n = char.codePointAt(0); if (!(n === 9 || n === 10 || n === 13 || (n >= 32 && n <= 0xD7FF) || (n >= 0xE000 && n <= 0xFFFD) || n >= 0x10000)) throw new Error('Invalid XML character'); } return String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[c]); } export function renderFeed(rows, origin, asOf) { const x = escapeXml; const publicRows = rows.map(doc => ({ doc, identity: describeDocument(doc, asOf, origin) })) .filter(row => row.identity) .sort((a, b) => order(b.doc.publishedAt) - order(a.doc.publishedAt)) .slice(0, 20); const items = publicRows.map(({ doc, identity }) => { // RSSのpubDateには時刻が必要。日付しかない文書ではこの任意項目を省略する。 const date = identity.datePublished; const pubDate = date?.includes('T') ? `<pubDate>${new Date(instant(date)).toUTCString()}</pubDate>` : ''; return `<item><title>${x(doc.title)}</title><link>${x(identity.canonical)}</link> <guid isPermaLink="false">${x(identity.id)}</guid>${pubDate} <description>${x(x(doc.summary))}</description></item>`; }).join(''); return `<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"><channel><title>架空の図書館ニュース</title> <link>${x(origin)}</link><description>最新の公開ニュース20件</description>${items}</channel></rss>`; } // 日付のみの項目の並べ替えには韓国の暦日の開始時点を使う。pubDateには使わない。 function order(value) { return value === null ? -Infinity : instant(value.length === 10 ? value + 'T00:00:00+09:00' : value); }

XMLの&や<などの文字は文書構文として使われるため、escapeXmlがテキストをエスケープする。RSSのdescriptionは受信側がHTMLとして読む場合があるので、要約には2回適用する。最初の変換はHTML内の文字を、2回目はそのHTMLを含むXML内の文字を保つ。許可されない制御文字はエラーにする。pubDateには初回公開時刻を入れ、日付しか分からない場合はこの任意項目を省略する。

本文を変更したら、同じguidの要約とリンクを更新する。このRSSには項目別の更新時刻を別途入れていない。更新時刻が必要な連携では、Atomのupdatedのように受信ツールがサポートするフィールドを選ぶ。撤回した記事は次のフィードから外れるが、購読ツールが保存済みの記事は残る場合がある。原文へのアクセスを遮断し、訂正の事実を別の記事で知らせるか決める必要がある。

画像実習のサーバーは/rss.xmlでこの結果をapplication/rss+xmlとして返す。ホームページのheadにはrel="alternate"リンクがある。RSSのURLを直接開いてXMLを読む検査と、実際の購読ツールが新規記事・同じIDの更新・撤回をどう処理するかを見る検査を分けて行う。

— FIN —