

Search Consoleを見たら、68ページが未登録、25ページのみ登録済みという状態だった。「検出 - インデックス未登録」が36ページもあり、しかもクロールすらされていない(「前回のクロール: 該当なし」)。
URLを確認すると、/blogs/328a0ffb73d1813eacd7f8a0ee759f1a のようなNotion page IDがそのままURLになっている記事ばかりだった。
「検出 - インデックス未登録」の原因特定方法
Notion CMSでSlugが未設定だとSEOにどう影響するか
Notion REST APIでSlugを一括設定する方法
カテゴリ/タグ/アーカイブページにmetadataを追加する方法
Google Indexing APIでクロールをリクエストする方法
Notion APIをCMSとして使っているブログ運営者
Search Consoleで「検出 - インデックス未登録」が多い方
Next.js App Routerでmetadata設定が不十分な方
Next.js 15 (App Router) + Notion REST API構成のブログ
Google Search Consoleに登録済み
GCPサービスアカウントでIndexing APIが有効化済み
Search Consoleのインデックス登録レポート:
| 指標 | 値 |
|---|---|
| インデックス登録済み | 25ページ |
| 未登録 | 68ページ |
| 「検出 - インデックス未登録」 | 36ページ |
URLを見ると、すべて Notion page IDがそのままURL になっていた:
https://example.com/blogs/328a0ffb73d1813eacd7f8a0ee759f1a
https://example.com/blogs/328a0ffb73d181429677ece0e30e193a
Googleはこれらを「意味のないランダム文字列のURL」と判断し、クロール優先度を下げていた。
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」と判断 → 「検出 - インデックス未登録」
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未設定記事が見つかった。
| タイトル | 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 |
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で設定完了。
カテゴリ・タグ・アーカイブページに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 },
};
}
タグ・アーカイブも同様に実装。
Slug設定だけではGoogleが新URLを知るまで時間がかかる。Indexing APIで能動的にリクエストする。
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']
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\"}"
done
18件すべてHTTP 200で成功。クォータは1日200リクエスト。
記事作成テンプレートにSlugを必須項目として追加し、品質チェックリストにも「Slugが設定されているか」を追加した。
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で18記事のクロールリクエスト
再発防止: Slug必須ルールを追加
Search Consoleで「検出 - インデックス未登録」の減少を経過観察する