Google Search Console 404エラー調査

未分類

Google Search Console 404エラー調査

問題の概要

Google Search Consoleで以下の2つの問題が報告された:

  1. 404エラー: 15件のURLが「見つかりませんでした(404)」
  2. インデックス登録数が少ない: サイトマップには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/

原因の可能性

  1. コンテンツが削除された: 該当のマークダウンファイルが存在しない
  2. パスの変更: ファイル名やディレクトリ構造が変更された
  3. トレイリングスラッシュの設定: 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/sitemap v7で自動生成
  • 更新タイミング: git push → GitHub Actions → ビルド時に自動更新
  • 現在のURL数: 177件
  • 最終更新: 2025-12-18

インデックス状況

  • サイトマップ登録: 177件
  • 実際のインデックス: 29件
  • 差異の原因: 以下の複合要因

インデックス数が少ない問題の分析

考えられる原因

原因説明対処
クロール遅延新しいサイトはクロール頻度が低い待つ(数週間〜数ヶ月)
薄いコンテンツ内容が少ないページは優先度が低いコンテンツを充実させる
重複コンテンツ類似ページがあると除外されるcanonical設定を確認
クロールバジェット大規模サイトでない限り問題なし-
noindex設定意図せずnoindexが設定されている確認が必要

Googleのインデックス処理

サイトマップ送信 → クロール予約 → クロール実行 → 品質評価 → インデックス登録
                    ↓                              ↓
               数日〜数週間                    品質が低いと除外

重要: サイトマップに登録 ≠ インデックスに登録

  • サイトマップはGoogleへの「クロールしてほしいURLリスト」
  • 実際にインデックスされるかはGoogleの品質判断による
  • 新しいサイトは数週間〜数ヶ月かけて徐々にインデックスされる

対処方法

  1. 待つ: 新しいコンテンツは時間がかかる(数週間〜数ヶ月)
  2. サイトマップ再送信: デプロイ後にSearch Consoleで再送信
  3. URL検査: 個別URLをSearch Consoleで検査・インデックス登録リクエスト
  4. 内部リンク強化: トップページや人気ページからリンクを張る

期待される改善

今回のSSG対応により:

  • coding-standards: 158ページが新たにインデックス対象に
  • コンテンツ品質: 静的HTMLで完全なコンテンツが提供される
  • メタデータ: title/descriptionが正しく設定される

次のアクション

  1. coding-standardsのプリレンダリング実装(完了)
  2. カスタムサイトマップ生成スクリプト作成(完了)
  3. デプロイして本番環境に反映
  4. Search Consoleでサイトマップを再送信
  5. 1〜2週間後にインデックス状況を再確認
  6. 必要に応じて個別URLの検査・登録リクエスト

変更したファイル一覧

ファイル変更内容
nuxt.config.tsnitro:config hookでプリレンダリングルートを動的追加
app/pages/coding-standards/viewer/[ruleId].vue<ClientOnly>削除、JSONを直接import、SSG対応
scripts/generate-sitemap.mjs新規作成: カスタムサイトマップ生成
package.jsonpostgenerateにサイトマップ生成を追加

参考リンク