【SEO】Google Search Consoleの「検出 - インデックス未登録」を18記事分解消した全手順

この記事でわかること
- Notion の page ID がそのまま URL になっていると、クロールさえされなくなるという話
- Slug 未設定の記事を洗い出して一括で入れる手順
- カテゴリ / タグ / アーカイブに generateMetadata を入れる必要性
- Indexing API は一般のブログ記事には効かない(200 が返っても騙されない)
Notion を CMS にした Next.js ブログを運用していること。Google Search Console にサイトを登録済みであること。
概要: Google Search Consoleで36ページが「検出 - インデックス未登録」になっていた問題を調査。Notion page IDがそのままURLになっていたことが主因と判明し、Slug一括設定・metadataの追加・Indexing APIでの登録リクエストで解消した全記録。
はじめに
Search Consoleを見たら、68ページが未登録、25ページのみ登録済みという状態だった。「検出 - インデックス未登録」が36ページもあり、しかもクロールすらされていない(「前回のクロール: 該当なし」)。
URLを確認すると、/blogs/328a0ffb73d1813eacd7f8a0ee759f1a のようなNotion page IDがそのままURLになっている記事ばかりだった。
1. 問題の全体像
Search Consoleのインデックス登録レポート:
| 指標 | 値 |
|---|---|
| インデックス登録済み | 25ページ |
| 未登録 | 68ページ |
| 「検出 - インデックス未登録」 | 36ページ |
URLを見ると、すべて Notion page IDがそのままURL になっていた:
https://example.com/blogs/328a0ffb73d1813eacd7f8a0ee759f1a
https://example.com/blogs/328a0ffb73d181429677ece0e30e193aGoogleはこれらを「意味のないランダム文字列のURL」と判断し、クロール優先度を下げていた。
2. 原因: NotionのSlugプロパティが未設定
Notion DBにはSlugプロパティ(rich_text型)があり、記事のURLスラッグとして使われる:
const slug = richTextToPlain(props.Slug?.rich_text || []);
const id = slug || page.id.replace(/-/g, '');
// Slugがなければpage IDがそのままURLになるSlugが空 → page.idがサイトマップに載る → Googleが「意味のないURL」と判断 → 「検出 - インデックス未登録」
3. 対策1: Slugの一括設定
3.1 未設定記事の洗い出し
curl -s -X POST \
"https://api.notion.com/v1/databases/${DATABASE_ID}/query" \
-H "Authorization: Bearer ${NOTION_API_KEY}" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d '{"page_size": 100}' | python3 -c "
import json, sys
data = json.load(sys.stdin)
for page in data['results']:
props = page['properties']
title = ''.join(
t['plain_text']
for t in props.get('Title',{}).get('title',[])
)
slug_arr = props.get('Slug',{}).get('rich_text',[])
slug = ''.join(t['plain_text'] for t in slug_arr)
if not slug:
print(f'{page[\"id\"]} | {title}')
"結果: 18件のSlug未設定記事が見つかった。
3.2 Slugの命名規則
| タイトル | Slug |
|---|---|
| Cloudflare Pages × Next.js のSSR応答を66倍高速化 | cloudflare-pages-ssr-66x-speedup |
| microCMSからNotion APIへCMS完全移行 | microcms-to-notion-api-migration |
| Next.js 13→16メジャーアップグレード | nextjs-13-to-16-upgrade |
3.3 一括設定
set_slug() {
local page_id="$1" slug="$2"
curl -s -o /dev/null -w "%{http_code}" \
-X PATCH "https://api.notion.com/v1/pages/${page_id}" \
-H "Authorization: Bearer ${NOTION_API_KEY}" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d "{\"properties\":{\"Slug\":{\"rich_text\":[{\"text\":{\"content\":\"${slug}\"}}]}}}"
}
set_slug "[page-id-1]" "notion-api-blog-system-nextjs"
set_slug "[page-id-2]" "cloudflare-pages-ssr-66x-speedup"18件すべてHTTP 200で設定完了。
4. 対策2: カテゴリ/タグ/アーカイブにmetadata追加
カテゴリ・タグ・アーカイブページにgenerateMetadata()が未設定だった。title/descriptionが親レイアウトの継承のみで、OGPもなかった。
import { Metadata } from 'next';
const siteUrl = process.env.SITE_URL || 'https://example.com';
export async function generateMetadata({
params,
}: {
params: Promise<{ categoryId: string; pageId: string }>;
}): Promise<Metadata> {
const { categoryId, pageId } = await params;
const category = await getCategoryDetail(
decodeURIComponent(categoryId)
).catch(() => null);
const name = category?.name
|| decodeURIComponent(categoryId);
const title = `${name}の記事一覧${
Number(pageId) > 1 ? ` (${pageId}ページ目)` : ''
}`;
const description =
`${name}に関する技術記事の一覧です。`;
const url =
`${siteUrl}/categories/${categoryId}/page/${pageId}`;
return {
title,
description,
alternates: { canonical: url },
openGraph: {
title, description, url, type: 'website',
},
twitter: { card: 'summary', title, description },
};
}タグ・アーカイブも同様に実装。
5. 対策3: Indexing API でクロールリクエスト(これは効かない)
Slug 設定だけでは Google が新 URL を知るまで時間がかかる。そこで Indexing API で能動的にリクエストしました。
この対策は効果がありません。後から調べて分かりました。Google の Indexing API は JobPosting(求人)と BroadcastEvent(ライブ配信)のページ専用で、通常のブログ記事は対象外です。
たちの悪いことに、対象外の URL を送っても API は HTTP 200 を返します。下の「18 件すべて HTTP 200 で成功」は、受け付けられたことしか意味していません。
一般のページで早くクロールさせたいなら、Search Console の「URL 検査」→「インデックス登録をリクエスト」を使います(手動ですがこちらは実際に効きます)。
実際に効いたのは対策 1(Slug 設定)と対策 2(metadata 追加)です。以下の手順は、JobPosting 等を扱うサイトでの参考として残しています。
5.1 認証
GCPサービスアカウントのJWTを生成しOAuth2トークンを取得:
import json, time, base64, urllib.request
import subprocess, tempfile
with open('/tmp/sc-auth.json') as f:
sa = json.load(f)
header = base64.urlsafe_b64encode(
json.dumps({"alg":"RS256","typ":"JWT"}).encode()
).rstrip(b'=')
now = int(time.time())
claims = {
"iss": sa["client_email"],
"scope":
"https://www.googleapis.com/auth/indexing",
"aud": sa["token_uri"],
"iat": now, "exp": now + 3600
}
payload = base64.urlsafe_b64encode(
json.dumps(claims).encode()
).rstrip(b'=')
# opensslで署名
with tempfile.NamedTemporaryFile(
mode='w', suffix='.pem', delete=False
) as kf:
kf.write(sa["private_key"])
key_file = kf.name
sign_input = header + b'.' + payload
result = subprocess.run(
['openssl','dgst','-sha256','-sign',key_file],
input=sign_input, capture_output=True
)
signature = base64.urlsafe_b64encode(
result.stdout
).rstrip(b'=')
jwt_token = (
header + b'.' + payload + b'.' + signature
).decode()
data = urllib.parse.urlencode({
'grant_type':
'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': jwt_token
}).encode()
req = urllib.request.Request(
sa["token_uri"], data=data
)
resp = urllib.request.urlopen(req)
access_token = json.loads(
resp.read()
)['access_token']5.2 リクエスト送信
TOKEN=$(cat /tmp/sc-token.txt)
for slug in \
"notion-api-blog-system-nextjs" \
"cloudflare-pages-ssr-66x-speedup" \
"microcms-to-notion-api-migration"; do
curl -s -X POST \
"https://indexing.googleapis.com/v3/\
url Notifications:publish" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"url\":\"https://example.com/blogs/${slug}\",\
\"type\":\"URL_UPDATED\"}"
done18件すべてHTTP 200で成功。クォータは1日200リクエスト。
6. 再発防止
記事作成テンプレートにSlugを必須項目として追加し、品質チェックリストにも「Slugが設定されているか」を追加した。
Tips
- Notion page IDがURLになるとGoogleに「低品質URL」と判断される。意味のあるSlugを必ず設定
generateMetadata()は各page.tsxで個別に定義が必要。layout.tsxの継承だけでは不十分- Indexing APIは
URL_UPDATEDとURL_DELETEDの2種類。非公開時はURL_DELETEDを送る - Indexing APIはクロールの「リクエスト」であり登録を保証するものではない
- サービスアカウントの鍵ファイルは処理後に必ず削除。gitにコミットしない
参考リンク
まとめ
- 原因: Notion page IDがそのままURLになり、Googleがクロール優先度を下げていた
- 対策1: 18記事にSEOフレンドリーなSlugを一括設定
- 対策2: カテゴリ/タグ/アーカイブに
generateMetadata()追加 - 対策3(効果なし): Indexing API でのクロールリクエスト。一般ページは対象外だった
- 再発防止: Slug 必須ルールを追加
- 早くクロールさせたいときは、Search Console の URL 検査から手動でリクエストする
更新履歴
- 対策3(Indexing API)の評価を訂正。Indexing API は JobPosting / BroadcastEvent 専用で一般のブログ記事には効かず、対象外の URL でも HTTP 200 が返るため「成功」に見えていたことを警告として追加(別記事の SEO 対策とも矛盾していた)。代替として Search Console の URL 検査を提示。まとめと Tips も合わせて修正。コードブロックの言語指定を javascript から text に修正。箸条書きの Tips を callout に統一。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。


