Google Search Console 404エラー調査
Google Search Console 404エラー調査
問題の概要
Google Search Consoleで以下の2つの問題が報告された:
- 404エラー: 15件のURLが「見つかりませんでした(404)」
- インデックス登録数が少ない: サイトマップには177件あるのに29件しか登録されていない
問題1: coding-standards の404エラー
症状
以下のパターンのURLが404を返している:
https://log.eurekapu.com/coding-standards/viewer/rule-8-5
https://log.eurekapu.com/coding-standards/viewer/rule-10-7
https://log.eurekapu.com/coding-standards/viewer/rule-10-6
... 他多数
原因
動的ルート/coding-standards/viewer/[ruleId].vueで、静的HTMLが生成されていない。
ビルド出力の確認
dist/coding-standards/viewer/
├── index.html ← これしかない
└── _payload.json
※ rule-8-5.html などは存在しない
コードの問題点
<!-- apps/web/app/pages/coding-standards/viewer/[ruleId].vue -->
<template>
<ClientOnly> <!-- ← クライアントサイドのみでレンダリング -->
...
</ClientOnly>
</template>
<ClientOnly>でラップされているため、SSG時に空のHTMLが生成される- クライアントサイドのみの動的ルートを、Googleボットは404として認識
- 内部リンク(前へ/次へボタン)からこれらのURLが発見され、クロール対象になっている
影響範囲
coding-standards.json には約100以上のルールが定義されており、それぞれの個別URLが404になっている。
問題2: トレイリングスラッシュの重複404
症状
同じページがスラッシュあり/なしの両方で404になっている:
https://log.eurekapu.com/2025-12-09/nuxt-vite-optimize-deps-warning
https://log.eurekapu.com/2025-12-09/nuxt-vite-optimize-deps-warning/
https://log.eurekapu.com/2025-12-06/nuxt-prefetch-guide
https://log.eurekapu.com/2025-12-06/nuxt-prefetch-guide/
原因の可能性
- コンテンツが削除された: 該当のマークダウンファイルが存在しない
- パスの変更: ファイル名やディレクトリ構造が変更された
- トレイリングスラッシュの設定: Nuxtのルーティング設定の問題
問題3: cdn-cgi の404
症状
https://log.eurekapu.com/cdn-cgi/l/email-protection
原因
これはCloudflareのメール保護機能に関連するURL。外部サイトからのリンクで発生した可能性が高い。対応不要。
実施した解決策: プリレンダリング(SSG対応)
選択肢Aを採用
全158ルールの静的HTMLを生成するよう動的ルートをSSG(Static Site Generation)対応にする。
実装内容
1. nuxt.config.ts の修正
// nuxt.config.ts
import { readFile } from "node:fs/promises";
// JSONからルートを動的生成
async function getCodingStandardsRoutes(): Promise<string[]> {
const jsonPath = resolve(__dirname, 'public/data/coding-standards.json');
const data = JSON.parse(await readFile(jsonPath, 'utf-8'));
return data.map((rule: { ruleNumber: string }) =>
`/coding-standards/viewer/rule-${rule.ruleNumber.replace('.', '-')}`
);
}
export default defineNuxtConfig({
hooks: {
'nitro:config': async (nitroConfig) => {
const routes = await getCodingStandardsRoutes();
nitroConfig.prerender.routes = [
...(nitroConfig.prerender.routes || []),
...routes
];
console.log(`✓ Added ${routes.length} coding-standards routes for prerendering`);
}
}
})
2. ruleId.vue のSSR対応
修正前(問題のあるコード):
<template>
<ClientOnly> <!-- ← SSG時に空のHTMLになる -->
<div v-if="currentRule">...</div>
</ClientOnly>
</template>
<script setup>
// クライアントサイドでfetch → SSGでは動作しない
const response = await fetch('/data/coding-standards.json');
</script>
修正後(SSG対応):
<template>
<!-- ClientOnlyを削除 → SSGで完全なHTMLが生成される -->
<div v-if="currentRule">...</div>
</template>
<script setup>
// 直接import → SSG時にデータが埋め込まれる
import codingStandardsData from '../../../../public/data/coding-standards.json';
const allRules = ref(codingStandardsData);
// highlight.jsはクライアントサイドのみで実行
async function highlightCode() {
if (import.meta.server) return; // SSR時はスキップ
const hljs = (await import('highlight.js/lib/core')).default;
// ...
}
</script>
試行錯誤: SSR対応で失敗したパターン
失敗1: useFetch を使用 → 500エラー
<script setup>
// ❌ SSG時に500エラーが発生
const { data: allRules } = await useFetch('/data/coding-standards.json', {
default: () => []
});
</script>
エラー内容:
[nitro] ├─ /coding-standards/viewer/rule-8-5 (766ms)
│ └── [500] Server Error
原因: SSG時にpublicディレクトリへのfetchが解決できない。
失敗2: ~/public パスでimport → ビルドエラー
<script setup>
// ❌ パスが解決できない
import codingStandardsData from '~/public/data/coding-standards.json';
</script>
エラー内容:
ERROR [vite:load-fallback] Could not load C:/Users/.../app//public/data/coding-standards.json
ENOENT: no such file or directory
原因: ~ エイリアスは app/ ディレクトリを指すため、public/ に到達できない。
成功: 相対パスでimport
<script setup>
// ✅ 相対パスで正しく解決される
import codingStandardsData from '../../../../public/data/coding-standards.json';
</script>
ポイント:
app/pages/coding-standards/viewer/[ruleId].vueからpublic/data/への相対パス- 4階層上がって
public/data/に到達
SSR/SSGの仕組み解説
なぜ <ClientOnly> だと404になるのか
┌─────────────────────────────────────────────────────────┐
│ ビルド時(SSG) │
├─────────────────────────────────────────────────────────┤
│ 1. Nuxtが /coding-standards/viewer/rule-8-5 をレンダリング │
│ 2. <ClientOnly> の中身はスキップされる │
│ 3. 空のHTMLが生成される │
│ │
│ 結果: Googleボットが見るのは空のページ → 404扱い │
└─────────────────────────────────────────────────────────┘
SSG対応後の流れ
┌─────────────────────────────────────────────────────────┐
│ ビルド時(SSG) │
├─────────────────────────────────────────────────────────┤
│ 1. JSONを直接import → データがビルドに含まれる │
│ 2. <ClientOnly>なし → 完全なHTMLが生成される │
│ 3. 各ルールページの静的HTMLがdist/に出力 │
│ │
│ 結果: Googleボットが見るのは完全なコンテンツ │
└─────────────────────────────────────────────────────────┘
ビルド出力の比較
修正前:
dist/coding-standards/viewer/
├── index.html
└── _payload.json
修正後:
dist/coding-standards/viewer/
├── index.html
├── rule-1-1/index.html ← 新規生成
├── rule-1-2/index.html ← 新規生成
├── rule-8-5/index.html ← 新規生成
├── ...(全158ルール)
└── _payload.json
ビルド結果
✓ Added 158 coding-standards routes for prerendering
Prerendered 579 routes in 35.078 seconds
✓ Generated sitemap.xml with 287 URLs
- Coding standards rules: 158
- Other pages: 129
- 追加ルート数: 158件(coding-standards)
- 総プリレンダリング数: 579ルート
- サイトマップURL数: 287件
- ビルド時間: 約35秒
落とし穴: @nuxtjs/sitemap と cloudflare-pages-static
問題
@nuxtjs/sitemap v7は cloudflare-pages-static プリセットと組み合わせると、空のサイトマップが生成される。
<!-- 生成されるsitemap.xml(空) -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="...">
</urlset>
<!-- XML Sitemap generated by @nuxtjs/sitemap v7.4.8 -->
試した方法(すべて失敗)
失敗1: sitemap.urls オプション
// nuxt.config.ts
sitemap: {
urls: async () => {
const data = JSON.parse(await readFile(jsonPath, 'utf-8'));
return data.map(rule => ({
loc: `/coding-standards/viewer/rule-${rule.ruleNumber.replace('.', '-')}`
}));
}
}
結果: サイトマップが完全に空になる(他のページも含まれない)
失敗2: sitemap.sources オプション
// nuxt.config.ts
sitemap: {
sources: ['/api/__sitemap__/coding-standards']
}
結果: APIエンドポイントが呼ばれない、サイトマップは空のまま
失敗3: サーバーAPIエンドポイント
// server/api/__sitemap__/coding-standards.ts
import { defineSitemapEventHandler } from '#imports'
import { readFileSync } from 'fs'
export default defineSitemapEventHandler(() => {
const data = JSON.parse(readFileSync(jsonPath, 'utf-8'))
return data.map(rule => ({ loc: `/coding-standards/viewer/rule-...` }))
})
エラー内容:
RollupError: Could not resolve "../../../../public/data/coding-standards.json"
原因: サーバーサイドのコードからの相対パスimportはRollupで解決できない。
process.cwd() を使ったパスも、SSG時には正しく動作しない。
解決策: カスタムスクリプト
ビルド後にカスタムスクリプトでサイトマップを生成する。
// scripts/generate-sitemap.mjs
import { readdirSync, writeFileSync } from 'fs'
import { join, resolve } from 'path'
const SITE_URL = 'https://log.eurekapu.com'
const DIST_DIR = resolve(process.cwd(), 'dist')
// distディレクトリから全HTMLファイルを検出
function getAllRoutes(dir, baseRoute = '') {
const routes = []
const entries = readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('_')) {
routes.push(...getAllRoutes(join(dir, entry.name), `${baseRoute}/${entry.name}`))
} else if (entry.name === 'index.html') {
routes.push(baseRoute || '/')
}
}
return routes
}
// サイトマップXMLを生成
const routes = getAllRoutes(DIST_DIR)
const sitemap = generateSitemap(routes)
writeFileSync(join(DIST_DIR, 'sitemap.xml'), sitemap)
// package.json
{
"scripts": {
"postgenerate": "node scripts/fix-routes.mjs && node scripts/generate-sitemap.mjs"
}
}
教訓
@nuxtjs/sitemapはSSR/ハイブリッドモードでは動作するが、完全な静的生成では問題がある- Cloudflare Pages Staticプリセット使用時は、カスタムスクリプトでサイトマップを生成するのが確実
他の選択肢(参考)
選択肢B: インデックスから除外
robots.txt で除外する方法。実装変更不要だが、検索で見つからなくなる。
User-Agent: *
Disallow: /coding-standards/viewer/rule-
選択肢C: 動的ルートを廃止
一覧ページのみに集約。シンプルだが直リンク不可。
調査で判明した追加事項
サイトマップの状態
- 生成方法:
@nuxtjs/sitemapv7で自動生成 - 更新タイミング:
git push→ GitHub Actions → ビルド時に自動更新 - 現在のURL数: 177件
- 最終更新: 2025-12-18
インデックス状況
- サイトマップ登録: 177件
- 実際のインデックス: 29件
- 差異の原因: 以下の複合要因
インデックス数が少ない問題の分析
考えられる原因
| 原因 | 説明 | 対処 |
|---|---|---|
| クロール遅延 | 新しいサイトはクロール頻度が低い | 待つ(数週間〜数ヶ月) |
| 薄いコンテンツ | 内容が少ないページは優先度が低い | コンテンツを充実させる |
| 重複コンテンツ | 類似ページがあると除外される | canonical設定を確認 |
| クロールバジェット | 大規模サイトでない限り問題なし | - |
| noindex設定 | 意図せずnoindexが設定されている | 確認が必要 |
Googleのインデックス処理
サイトマップ送信 → クロール予約 → クロール実行 → 品質評価 → インデックス登録
↓ ↓
数日〜数週間 品質が低いと除外
重要: サイトマップに登録 ≠ インデックスに登録
- サイトマップはGoogleへの「クロールしてほしいURLリスト」
- 実際にインデックスされるかはGoogleの品質判断による
- 新しいサイトは数週間〜数ヶ月かけて徐々にインデックスされる
対処方法
- 待つ: 新しいコンテンツは時間がかかる(数週間〜数ヶ月)
- サイトマップ再送信: デプロイ後にSearch Consoleで再送信
- URL検査: 個別URLをSearch Consoleで検査・インデックス登録リクエスト
- 内部リンク強化: トップページや人気ページからリンクを張る
期待される改善
今回のSSG対応により:
- coding-standards: 158ページが新たにインデックス対象に
- コンテンツ品質: 静的HTMLで完全なコンテンツが提供される
- メタデータ: title/descriptionが正しく設定される
次のアクション
- coding-standardsのプリレンダリング実装(完了)
- カスタムサイトマップ生成スクリプト作成(完了)
- デプロイして本番環境に反映
- Search Consoleでサイトマップを再送信
- 1〜2週間後にインデックス状況を再確認
- 必要に応じて個別URLの検査・登録リクエスト
変更したファイル一覧
| ファイル | 変更内容 |
|---|---|
nuxt.config.ts | nitro:config hookでプリレンダリングルートを動的追加 |
app/pages/coding-standards/viewer/[ruleId].vue | <ClientOnly>削除、JSONを直接import、SSG対応 |
scripts/generate-sitemap.mjs | 新規作成: カスタムサイトマップ生成 |
package.json | postgenerateにサイトマップ生成を追加 |