KiSAKU
技術系8分で読めます

推測させるのをやめる

公開

検索エンジンに、推測させるのをやめる。

今回入れたのは3つです。sitemap.xmlrobots.txt、そして構造化データ。バラバラの施策に見えますが、やっていることは同じです。これまで相手に推測させていたことを、こちらから明示的に伝える。それだけです。

この記事で扱うこと

3つがそれぞれ何を伝えているか、順位と発見速度は別物だという話、そして実装時に踏んだ「一覧用のクエリを流用してはいけなかった」という落とし穴。SEOの小手先ではなく、仕組みとして何が起きているかの話です。

3つとも「伝える」ための仕組み

先に全体像を置きます。

伝えていること 無いとどうなるか
sitemap.xml どんなページが存在するか リンクをたどって発見されるまで待つ
robots.txt どこを見てよいか / 地図の場所 全部を手探りで巡回される
構造化データ そのページが何なのか HTMLの見た目から推測される

右の列に共通しているのは、「無くても動くが、相手の推測に任せることになる」という点です。推測は当たることもあれば外れることもあり、こちらからは制御できません。

sitemap.xml — 順位ではなく、見つかるまでの時間

まず誤解を解いておきます。sitemapを置いても、検索順位は上がりません。

これは「見つけてもらうまでの時間」を短くするものです。検索エンジンは通常、リンクをたどってページを見つけます。外部からのリンクがまだ少ない新しいサイトでは、存在しているのに気づかれない期間が生まれます。sitemapは、そのたどる作業を省いて一覧を直接渡す仕組みです。

Next.jsのApp Routerでは、app/sitemap.ts に既定の関数を書くだけで /sitemap.xml が生成されます。XMLを自分で組み立てる必要はありません。

app/sitemap.ts(要点)
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const staticEntries = [
    { url: `${SITE_URL}/`,     changeFrequency: "weekly",  priority: 1   },
    { url: `${SITE_URL}/blog`, changeFrequency: "weekly",  priority: 0.8 },
    // ...
  ];

  const posts = await getPostsForSitemap();
  // ...
}
priority は「相対的な」重要度

0から1の値ですが、これは検索エンジンに対する評価の申告ではありません。サイト内での相対的な優先度です。全部を1.0にすると差が無くなり、指定していないのと同じになります。

20本の壁

実装中に気づいた問題です。ここが今回いちばんの学びでした。

記事一覧を取るための関数は、すでに用意してありました。getPosts() です。sitemapもこれを使えば済む——と思ったのですが、中身を見て手が止まりました。

一覧用のクエリ
query GetPosts {
  posts(first: 20) {   ← 20本で打ち切っている
    ...
  }
}

一覧ページに20本並べる、という表示上の都合でこうなっていました。これをsitemapに流用すると、21本目以降は「存在するのに検索エンジンへ知らせない記事」になります。

気づかないまま進む種類の不具合

この漏れは、エラーになりません。sitemapは正常に生成され、中身も正しく見えます。足りないことに気づく手がかりが、どこにもありません。公開記事がちょうど20本だったので、次の1本を公開した瞬間から静かに始まるところでした。

教訓は単純です。「一覧に何本並べるか」と「検索エンジンに何本知らせるか」は別の話でした。同じ「記事一覧」という名前がついているせいで、同じものに見えていただけです。

sitemap専用に、全件を取る関数を分けました。

型生成への依存を、持ち込まない

ここでもうひとつ判断が要りました。

このプロジェクトのGraphQLクエリは、graphql-codegen が生成した型付きのドキュメントを経由しています。型が付くのは大きな利点ですが、制約がついてきます。クエリを1文字でも変えたら、型を作り直さなければなりません。そして作り直しにはCMSへ接続してスキーマを読む必要があります。

得られるもの 引き換えに背負うもの
型付きクエリ 取り違えを防げる CMSに繋がる環境でしか変更できない
素の文字列 どこでも書ける 型は自分で書く

sitemapが必要としているのは slugmodified の2つだけです。2つしか無いものを、取り違えようがありません。ここだけ素の文字列で問い合わせ、型は手で書くことにしました。

仕組みの利点は、全部に適用しなくてよい

型生成は良い仕組みですが、それに乗ることで「CMSに繋がらないと作業できない」という性質も一緒に受け取ります。その性質が邪魔になる場所だけ、外れてよいと判断しました。一貫性のために全部を揃えると、揃えた不便さも全部に広がります。

robots.txt — 守っていないもの

app/robots.ts を置くと /robots.txt が生成されます。中身はこれだけです。

生成された robots.txt
User-Agent: *
Allow: /

Host: https://www.kisaku.site
Sitemap: https://www.kisaku.site/sitemap.xml

ここで大事なのは、robots.txt はアクセス制限ではないことです。「クロールしてよい場所」を伝える取り決めであって、強制力はありません。守るかどうかは相手次第です。

隠したいものを書いてはいけない

「見られたくないから Disallow に書く」という使い方は逆効果です。robots.txt は誰でも読めるので、「ここに何かある」と書き残すことになります。本当に見せたくないものは、認証をかけるか、そもそも公開領域に置かないかのどちらかです。

今回の実質的な役割は最後の1行、sitemapの場所を知らせることです。これがあると、Search Consoleに登録していない検索エンジンもsitemapを見つけられます。

Host を書いているのは、このサイトが kisaku.sitewww 側へ寄せているためです。どちらが正なのかを明示しています。

構造化データ — 見た目から意味を切り離す

3つめです。記事ページに、こういうものを埋め込みました。

出力されるJSON-LD(抜粋)
{
  "@type": "BlogPosting",
  "headline": "wwwのあるなしで、別のサイトになる",
  "datePublished": "2026-08-22T13:20:00",
  "dateModified":  "2026-08-22T14:21:46",
  "author":    { "@type": "Person", "name": "KiSAKU" },
  "publisher": { "@type": "Person", "name": "KiSAKU" }
}

画面には何も出ません。「これは記事で、公開日はこれで、書いたのはこの人」と直接伝えているだけです。

これが無い場合、検索エンジンはHTMLの構造から推測します。「大きな文字だから見出しだろう」「日付っぽい文字列があるから公開日だろう」と。たいてい当たりますが、デザインを変えると解釈が変わることがあります。

見た目を変えても、意味は動かない

明示しておけば、日付の表示位置を変えても、見出しのサイズを変えても、伝わる内容は同じままです。装飾の変更が、意図せず情報を壊さない状態になります。

パンくずも一緒に出す

BreadcrumbList も併記しています。検索結果に出るパンくず表示のためです。

階層
ホーム › ブログ › (記事タイトル)

ここで書く階層は、画面上の構造と一致させる必要があります。実際には無い階層を書くと、検索結果と実際のサイトが食い違い、逆効果になります。

タグが途中で閉じないようにする

細かいですが、実装上の注意点です。JSON-LDは <script> タグの中に書きます。もし記事タイトルに </script> という文字列が入っていたら、そこでタグが閉じてしまいます。

対処
JSON.stringify(jsonLd).replace(/</g, "\\u003c")

< をユニコードのエスケープ表記に置き換えています。JSONとしての意味は変わらず、HTMLとしては無害になります。自分で書いた記事しか入らない場所でも、こういう箇所は塞いでおくほうが安いです。

入れたら、実物で確かめる

3つとも、公開後に実際のURLを叩いて確認しました。

確認
https://www.kisaku.site/robots.txt   → 4行が返る
https://www.kisaku.site/sitemap.xml  → 固定ページ5 + 記事20 = 25件
記事ページ → script[type="application/ld+json"] を読んで中身を検証

特にsitemapは、件数を数えるまで安心できません。今回の20本の壁のように、正しく生成されているのに中身が足りない、という失敗があり得るからです。「エラーが出ていない」は「正しい」ではありません。

今回の学び

まず、3つとも「順位を上げる」ものではないこと。sitemapは発見までの時間を短くし、robots.txtは地図の場所を教え、構造化データは意味の推測を減らします。効果を正しく理解していないと、成果が出ないときに何を疑えばいいか分からなくなります。

次に、似た名前のものを、同じものだと思わないこと。「記事一覧」という同じ名前でも、表示のための一覧と、検索エンジンへ渡す一覧は要件が違いました。流用は速いですが、要件が違うものを流用すると、後から静かに壊れます。

そして、仕組みの利点は、全部に適用しなくてよいこと。型生成は良い仕組みですが、乗ると「CMSに繋がらないと変更できない」という性質も付いてきます。その性質が邪魔になる1か所だけ、外れる判断をしました。

最後に、エラーが出ないことは、正しいことではないこと。sitemapの20本上限は、何のエラーも出さずに記事を落とし続けるところでした。出力されたものを数える、という確認をしなければ気づけませんでした。

← ブログ一覧へ戻る