Next.js で個人開発のサイトを作っています。無料の計算ツールを集めた ShibaHub というサイトで、いまツールが155個あります。
今日、Search Console を調べていて 「2ページだけ、Googleが存在すら知らない」 という状態を見つけました。原因を追っていったら、自分で書いた sitemap.ts の 1行に行き着いたので、その話を書きます。
初心者の方でも追えるように、用語は都度説明します。
そもそも sitemap.xml とは
「うちのサイトにはこういうページがありますよ」という一覧をGoogleに渡すファイルです。
Next.js(App Router)だと app/sitemap.ts を置くだけで自動生成してくれます。こんな形です。
// app/sitemap.ts
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: "https://example.com/a", lastModified: new Date() },
{ url: "https://example.com/b", lastModified: new Date() },
];
}
lastModified は 「そのページを最後に更新した日」 です。出力されるXMLだとこうなります。
<url>
<loc>https://example.com/a</loc>
<lastmod>2026-08-08T09:00:00.000Z</lastmod>
</url>
何が起きていたか
まず、おかしな数字に気づきました。Search Console で、投資系のツール3本が 90日間で表示回数0 だったんです。
「表示回数0」は「検索結果に一度も出ていない」という意味です。順位が低いのとは違います。そもそも土俵に上がっていない状態です。
そこで URL検査API で1本ずつ調べました。Search Console の画面にある「URL検査」を、APIから叩けるやつです。
from google.oauth2 import service_account
from googleapiclient.discovery import build
creds = service_account.Credentials.from_service_account_file(
"service-account.json",
scopes=["https://www.googleapis.com/auth/webmasters"],
)
sc = build("searchconsole", "v1", credentials=creds)
r = sc.urlInspection().index().inspect(body={
"inspectionUrl": "https://shibahub.jp/tools/compound-interest-calculator",
"siteUrl": "sc-domain:shibahub.jp",
"languageCode": "ja",
}).execute()
s = r["inspectionResult"]["indexStatusResult"]
print(s.get("verdict")) # PASS / NEUTRAL / FAIL
print(s.get("coverageState")) # 「送信して登録されました」など
print(s.get("lastCrawlTime")) # 最後にクロールされた日時
結果がこれでした。
| ページ | verdict | 最終クロール |
|---|---|---|
| 複利計算 | NEUTRAL(URLが認識されていません) | なし |
| 積立シミュレーター | NEUTRAL(URLが認識されていません) | なし |
| 72の法則 | PASS(登録済み) | 2026-07-26 |
| 利回り計算 | PASS(登録済み) | 2026-07-25 |
上2本は、Googleが一度も見に来ていませんでした。
順位が低いのではなく、存在を知られていない。これは全然違う問題です。
犯人探し
まず思いつく原因を順に潰しました。
| 疑ったこと | 結果 |
|---|---|
| ページが404では? | 4本とも200 |
| sitemapに入っていない? | 4本とも入っている(全322URL) |
| どこからもリンクされていない? | 4本とも同じ場所からリンクされている |
| 作ったばかりで、まだ順番が来ていない? | 4本とも2026-07-02作成。5週間経っている |
条件がほぼ同じなのに、2本だけ来ていない。 ここで手が止まりました。
で、sitemap を生成しているコードを見返しました。
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date(); // ← ビルド時刻
const tools = TOOLS.map((tool) => ({
url: `${BASE}/tools/${tool.slug}`,
lastModified: now, // ★ 155本ぜんぶ「今日」
changeFrequency: "monthly",
priority: 0.9,
}));
...
}
155ページ全部の lastmod に、ビルド時刻を入れていました。
sitemap.ts はビルド時に実行されるので、new Date() は「デプロイした瞬間の時刻」になります。つまり、
デプロイするたびに、1文字も直していないページまで「今日更新しました」とGoogleに申告していた
ということです。私は毎日のようにデプロイしているので、Googleから見ると 「毎日155ページ全部が更新されるサイト」 に見えていたことになります。
なぜこれがまずいのか
Googleは lastmod を使います。ただし条件があって、公式のドキュメントにこう書かれています。
Googleは、一貫して正確であることが確認できる場合に
lastmodの値を使用します
裏を返すと、当てにならないと判断されたら丸ごと無視されるということです。
無視されると何が起きるか。Googleは自分の判断でクロールの優先順位を決めます。クロールできる量には上限(クロールバジェット)があるので、優先度の低いページはいつまでも順番が回ってきません。
今回の2本は、まさにそれに見えました。
⚠️ これは「証明できた」わけではありません。Googleの内部の判断は見えないので、条件が同じ4本の中で見つかった唯一の差がここだった、というところまでです。ただ、直さない理由もありません。
直し方:git から本当の更新日を拾う
各ページを最後に触ったコミットの日時を git log から取って、JSONに書き出しました。
// scripts/gen-lastmod.mjs
import { execFileSync } from "node:child_process";
import { readdirSync, writeFileSync } from "node:fs";
function lastCommitISO(relPath) {
try {
return execFileSync("git", ["log", "-1", "--format=%cI", "--", relPath], {
encoding: "utf8",
}).trim() || null;
} catch {
return null;
}
}
const map = {};
for (const entry of readdirSync("app/tools", { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const iso = lastCommitISO(`app/tools/${entry.name}`);
if (iso) map[`/tools/${entry.name}`] = iso;
}
writeFileSync("lib/lastmod.json", JSON.stringify(map, null, 2));
git log -1 --format=%cI -- <パス> で、そのパスを最後に変更したコミットの日時がISO形式で取れます。
sitemap 側はこれを読むだけです。
import LASTMOD from "@/lib/lastmod.json";
function lastmodOf(path: string, fallback: Date): Date {
const iso = (LASTMOD as Record<string, string>)[path];
return iso ? new Date(iso) : fallback; // ★無ければ従来どおり
}
const tools = TOOLS.map((tool) => ({
url: `${BASE}/tools/${tool.slug}`,
lastModified: lastmodOf(`/tools/${tool.slug}`, now),
changeFrequency: "monthly",
priority: 0.9,
}));
★ビルド時に git を叩かなかった理由
最初は sitemap.ts の中で直接 git log を呼ぼうと思いました。でもやめました。
Vercelなどのビルド環境は、リポジトリを浅くクローン(shallow clone)することがあるからです。履歴が切り詰められていると git log が期待した日付を返しません。しかもビルドは通ってしまうので、間違った日付が出ていることに気づけません。
なので 手元で生成してコミットする方式にしました。副産物として、「いつ生成したか」がgitの履歴に残ります。
⚠️ 代わりに ページを直したら再生成する手間が増えます。忘れてもビルドは通るので、そこは割り切りです(忘れたら「更新したのに知らせていない」状態になるだけで、壊れはしません)。
結果
本番の sitemap.xml を確認しました。
| 直す前 | 直したあと | |
|---|---|---|
| URL数 | 322 | 322 |
| lastmod の日付の種類 | 1種類 | 11種類 |
2026-07-02 281件
2026-07-11 4件
2026-07-12 6件
2026-07-17 7件
2026-07-28 3件
...
2026-08-08 12件
281件が7月2日のままですが、それが事実なので正しい状態です。「全部が今日」より、「ほとんど動いていないが12件だけ今日動いた」のほうが、はるかに情報量があります。
なお、クロールが実際に来るかどうかはこれから数日〜数週間見ないと分かりません。効果が出たかどうかはまだ書けません。
ついでにやったこと
もうひとつ、この4本には 関連ツールへのリンクが1本も無かったので、相互にリンクしました。
インデックス済みの2本(利回り計算・72の法則)から、未クロールの2本(複利計算・積立シミュレーター)へ送る形です。トピックとしても自然な並びなので、ユーザーにとっても悪くないはずです。
まとめ
-
sitemap.tsでlastModified: new Date()を全ページに入れると、デプロイのたびに全ページが「今日更新」になる - Googleは
lastmodを使うが、一貫して正確なときだけ。当てにならないと無視される -
「表示回数0」は「順位が低い」ではないことがある。 URL検査APIで
lastCrawlTimeを見れば、クロールされていないのかどうかが分かる - git から本当の更新日を拾うのは簡単。ただしビルド環境で
git logを叩くのは避ける(浅いクローンで壊れる)
同じように new Date() を全ページに入れている方、けっこう多いんじゃないかと思います。一度自分のサイトの sitemap.xml を開いて、lastmod が全部同じ日付になっていないか見てみてください。