URL構造の改善計画
決定事項サマリー
| 項目 | 決定内容 |
|---|---|
| URL設計 | ルート直下 /{slug} |
| カテゴリ分け | 不要(全コンテンツ同一体系) |
| 重複slug | 統合(同一トピックは1記事に) |
| リダイレクト実装 | _redirectsファイル(Cloudflare Pages標準) |
| ファイル構造 | 現状維持(日付ディレクトリのまま、URLのみ変更) |
| 移行方式 | 一括移行 |
URL設計の根拠
- ドメイン名が
log.eurekapu.comのため、/blog/や/log/プレフィックスは冗長 - 「Blog」の語源は「Web Log」であり、log = blog
- カテゴリ分け(prompts, tutorials等)は現状不要(該当ディレクトリなし)
移行対象(対応後の数値)
| 項目 | 対応前 | 対応後 | 備考 |
|---|---|---|---|
| 公開対象ファイル | 320 | 318 | sample-article 2件削除 |
path 明示済み | 126 | 126 | path変換のみ |
path 未設定 | 194 | 192 | path追加が必要 |
| 重複slug(公開) | 4種類10件 | 0 | リネーム・削除で解消 |
| 予約語衝突 | 1件 | 0 | slug変更で解消 |
| 内部リンク更新 | 56件 | 0 | 28ファイルを更新 |
現状分析(2026-01-01時点)
コンテンツ統計
| 項目 | 数値 | 備考 |
|---|---|---|
| 総ファイル数 | 338 | .md ファイル |
| frontmatter + title あり | 320 | 公開対象 |
| frontmatter or title なし | 18 | 非公開メモ(drafts/ へ移動) |
path 明示済み | 126 | 明示的にpathを指定 |
path 未設定 | 194 | ファイルパスから自動生成 |
対応方針(確定): 非公開メモ(18件)は content/ 外の drafts/ ディレクトリへ移動する。
- 移動後は Nuxt Content の対象外となり、公開されない
- 衝突判定の対象からも除外される
- 最もシンプルで実装不要
path未設定ファイルの扱い
Nuxt Content v3の動作: path が未設定でもファイルパスから自動生成され、公開される。
content/2025-10-18/article.md → /2025-10-18/article(公開中)
したがって、移行対象は path 明示済みの126件ではなく、公開対象の318件(320件から sample-article 2件削除後)。
重複slugの現状(対応前)
日付を外した場合に衝突するslugが存在:
| slug | 重複ファイル | 対応方針 | 備考 |
|---|---|---|---|
claude-code-best-practices | 2025-11-01, 2025-12-30 | 11月版をリネーム | 公開 |
development-todo | 2025-12-16, 12-18, 12-21, 12-24 | 日付サフィックス追加 | 公開 |
handover-note | 2025-12-07, 2025-12-08 | 日付サフィックス追加 | 公開 |
sample-article | 2025-11-09, 2025-11-16 | 削除(テスト用) | 公開 |
README | 2025-11-28, 2025-12-03 | drafts/ へ移動 | 非公開(titleなし) |
集計:
- 公開ファイル内の重複: 4種類10ファイル
- 非公開含む全体: 5種類12ファイル
対応後: 重複ゼロ(リネーム3種類8件 + 削除1種類2件)
算出方法: 公開対象ファイル(frontmatter+titleあり)のファイル名を小文字正規化して集計。
予約語との衝突
予約語チェックで 1件の衝突 を検出:
| ファイル | 衝突する予約語 |
|---|---|
content/2025-11-21/index.md | index(Vueページ) |
→ slug を index 以外に変更が必要。
予約語(衝突禁止slug)
以下のslugはシステムで使用済みのため、コンテンツのslugとして使用不可:
| カテゴリ | 予約語(正規化後) |
|---|---|
| Vueページ | about, animation-demo, animation-demo-1, animation-demo-2, blog, coding-standards, excel-viewer, financial-data, financial-quiz, flowchart, index, japanese-writing-quiz, search, share |
| 静的アセット | api, audio, data, excel, images, js, favicon |
| システム | _nuxt, sitemap, robots, rss |
slug正規化ルール(予約語・コンテンツ両方に適用):
- 大文字小文字: 区別しない(
READMEとreadmeは衝突とみなす) - 比較時: 小文字に変換して判定
- 拡張子: 除去して比較(
sitemap.xml→sitemap、favicon.ico→favicon) - 記号: ファイル名に使用可能な文字のみ(
-,_は許可)
対策: 移行スクリプトで衝突検知を実装し、該当slugがあればエラーを出力する。
現状の問題
1. 日付ベースURLの課題
現在のURL構造: /2025-12-26/article-slug
| 問題点 | 詳細 |
|---|---|
| SEO | 古い日付のURLは「古いコンテンツ」と認識されやすい |
| 更新時の矛盾 | 内容を更新しても /2025-09-01/... のまま |
| 長期運用 | 10年運用で約3,650個の日付ディレクトリが生まれる |
| 意味の重複 | publishedAt / updatedAt があるのにURLにも日付 |
2. ファイル管理の煩雑さ
- 古いコンテンツが日付ディレクトリで散らばっている
- 見返すことが少ない古い記事がディレクトリ一覧を圧迫
調査結果
Nuxt Contentのpath生成の仕組み
重要: Nuxt Content v3は2つの方法でpathを決定する。
| ケース | 動作 |
|---|---|
frontmatterにpathあり | そのpathを使用 |
frontmatterにpathなし | ファイルパスから自動生成 |
# pathなしの場合(現状の多くのファイル)
content/2025-11-27/article.md → /2025-11-27/article
# pathありの場合
content/2025-11-27/article.md
---
path: "/blog/article" ← これが優先される
---
この挙動が有効になる条件
前提条件: コレクションのスキーマで path フィールドを明示的に定義していること。
// content.config.ts
export default defineContentConfig({
collections: {
pages: defineCollection({
type: "page",
source: "**/*.{md,mdx,mdc}",
schema: z.object({
// ...
path: z.string().optional(), // ← これが必須
// ...
})
})
}
});
Nuxt Content v3のドキュメントより:
For page-type collections,
pathis automatically generated. You can override any of these fields by defining them in the collection's schema.
つまり、このプロジェクトでは content.config.ts で path をスキーマに定義しているため、frontmatterの path が自動生成パスをオーバーライドする。これはNuxt Content v3の標準機能であり、カスタム実装ではない。
参考: Nuxt Content - Collection Types
移行時の注意
URL構造を変更する場合、以下の対応が必要:
- 新規コンテンツ: frontmatterに明示的に
pathを指定 - 既存コンテンツ:
pathフィールドを追加(新URL)- または、ファイル移動+旧URLリダイレクト
主要テックブログのURL設計
| サービス | URL形式 | 日付 |
|---|---|---|
| Zenn | /articles/{slug} | なし |
| Qiita | /items/{id} | なし |
| Medium | /{slug}-{hash} | なし |
| dev.to | /{user}/{slug} | なし |
→ 日付なしが主流。日付はメタデータで管理。
現在の実装仕様
// [...slug].vue
queryCollection("pages").path(docPath.value).first()
重要な発見: ルーティングは frontmatter の path フィールド を参照している。
つまり:
- ファイル構造とURLは独立
pathフィールドを変更するだけでURLを変更可能- ファイル移動なしでURL体系を刷新できる
移行後のイメージ
frontmatter例
# ファイル: content/2025-12-31/ai-deep-funnel.md(ファイル位置は変更なし)
---
title: "AI Deep Funnel"
path: "/ai-deep-funnel" # ← 日付なしのルート直下
publishedAt: "2025-12-31"
updatedAt: "2026-01-01"
---
ファイル構造
content/
├── 2025-10-17/ # 日付ディレクトリは維持(変更なし)
│ └── article.md
├── 2025-12-31/
│ └── ai-deep-funnel.md
└── 2026-01-01/
└── url-structure-improvement.md
→ ファイル構造は現状維持。URLのみ path フィールドで制御。
URL設計(確定)
| 変更前 | 変更後 |
|---|---|
/2025-12-31/ai-deep-funnel | /ai-deep-funnel |
/2025-10-17/kindle-publishing-howto | /kindle-publishing-howto |
/about | /about(変更なし) |
全コンテンツをルート直下 /{slug} で統一。カテゴリプレフィックスは使用しない。
移行時の注意点
- 既存の外部リンク: 旧URLへのリンクが切れないよう
_redirectsで301リダイレクト必須 - Google Search Console: 移行完了後にサイトマップ再送信
- 内部リンク: コンテンツ内の内部リンクも新URLに更新が必要
- OGP/canonical: 新URLで正しく動作することを確認
- 画像の相対パス: 絶対パスへの変更が必要(下記参照)
画像パスの問題(2026-01-01 追記)
問題: path フィールドでURLを変更すると、マークダウン内の相対画像パスが壊れる。
# 変更前(path未設定)
ファイル: content/2025-12-31/article.md
URL: /2025-12-31/article
画像:  → /2025-12-31/image.png ✅
# 変更後(pathでURL変更)
ファイル: content/2025-12-31/article.md
URL: /article(pathで上書き)
画像:  → /image.png ❌(404)
原因: ブラウザは現在のURLを基準に相対パスを解決する。URLが /article の場合、image.png は /image.png に解決されるが、実際の画像は /2025-12-31/image.png に配置されている。
解決策: 画像パスを絶対パスに変更する。
# 修正前(相対パス)

# 修正後(絶対パス)

対象ファイル: path フィールドを持ち、かつ相対画像パスを使用しているファイル(24件)。
検出コマンド:
cd apps/web/content
for f in $(grep -r "^path:" --include="*.md" -l); do
if grep -q '!\[.*\]([^/h][^)]*\.\(png\|jpg\|jpeg\|gif\|webp\))' "$f" 2>/dev/null; then
echo "$f"
fi
done
代替案: nuxt-content-assets モジュールを導入すれば、相対パスを自動で絶対パスに変換できる。ただし追加の依存関係が増える。
参考: nuxt-content-assets · Nuxt Modules
実装手順
Step 1: 重複slugの解消
以下の5種類12ファイルを個別対応する:
| slug | 対象ファイル | 対応方針 |
|---|---|---|
claude-code-best-practices | 2025-11-01, 2025-12-30 | 11月版を claude-code-best-practices-anthropic にリネーム(Anthropicブログ翻訳)、12月版はそのまま |
development-todo | 2025-12-16, 12-18, 12-21, 12-24 | 各ファイルに日付サフィックス追加: development-todo-2025-12-16 等 |
handover-note | 2025-12-07, 2025-12-08 | 各ファイルに日付サフィックス追加: handover-note-2025-12-07 等 |
README | 2025-11-28, 2025-12-03 | 非公開(プロジェクト固有README)→ drafts/ へ移動 |
sample-article | 2025-11-09, 2025-11-16 | 削除(テスト用ファイル) |
Step 2: frontmatter一括更新
318ファイルの path フィールドを更新するスクリプトを作成・実行:
// スクリプトの処理内容
// 1. path明示済み(126件): 日付部分を除去
// 旧: path: "/2025-12-31/ai-deep-funnel"
// 新: path: "/ai-deep-funnel"
//
// 2. path未設定(192件): ファイルパスからpathを生成して追加
// ファイル: content/2025-10-18/article.md
// 追加: path: "/article"
//
// 3. 予約語チェック: 衝突があればエラー出力
// 4. 重複チェック: 同一slugがあればエラー出力
Step 3: _redirectsファイル生成
public/_redirects に旧URL→新URLのマッピングを出力:
/2025-12-31/ai-deep-funnel /ai-deep-funnel 301
/2025-10-17/kindle-publishing-howto /kindle-publishing-howto 301
# ... 全318件
リダイレクト削除基準:
- 最短: Google Search Consoleで旧URLのインプレッションがゼロになってから1ヶ月後
- 推奨: 移行後6ヶ月経過、かつCloudflare Analyticsで旧URLへのアクセスがゼロ
- 安全策: テキストファイルでコストゼロのため永続的に残す
Step 4: 画像の相対パス修正
path フィールドを持つファイル内の相対画像パスを絶対パスに変換:
// 変換パターン:
//  → 
//  → 
//
// ファイルのディレクトリパスから絶対パスを生成
対象: 24ファイル(pathあり + 相対画像パス使用)
Step 5: 内部リンク更新
コンテンツ内の内部リンクを新URLに更新:
// 対象: 全マークダウンファイル内のリンク
// 変換パターン:
// 絶対パス: [記事](/ai-deep-funnel) → [記事](/ai-deep-funnel)
// 相対パス: [記事](/ai-deep-funnel) → [記事](/ai-deep-funnel)
// 完全URL: [記事](/ai-deep-funnel) → [記事](/ai-deep-funnel)
検出コマンド(全パターン対応):
# 絶対パス・相対パス
grep -rE "\(/?\d{4}-\d{2}-\d{2}/" content/
# 完全URL
grep -rE "https?://log\.eurekapu\.com/\d{4}-\d{2}-\d{2}/" content/
Step 6: 動作確認
- 全新URLでコンテンツが表示されること
- 全旧URLが新URLにリダイレクトされること
- 内部リンクが正しく動作すること
- OGP/canonicalが新URLを指していること
テストコード(TDD形式)
テストランナー: Vitest(pnpm test tests/url-migration.test.ts)
パス解決: __dirname 基準(実行場所に依存しない)
テストは2つのスイートで構成:
1. 現状確認テスト(移行前にPASS)
移行作業の前提条件を検証。これらが失敗した場合、ファイル構成が想定と異なる。
describe('現状確認(移行前)', () => {
it('総ファイル数が338件')
it('公開対象が320件')
it('公開ファイル内の重複slugが4種類存在する') // READMEは非公開
it('index.mdが予約語と衝突している')
it('日付パターンを含む内部リンクが56件存在する')
})
2. 移行完了条件テスト(TDD: 今FAIL → 移行後PASS)
it.fails を使用: アサーションが失敗することを期待。
describe('移行完了条件(現在FAIL → 移行後PASS)', () => {
it.fails('全slugがユニーク')
it.fails('予約語と衝突するslugがない')
it.fails('全ファイルにtitleがある')
it.fails('日付パターンを含む内部リンクがない')
it.fails('全公開ファイルにpathフィールドがある')
it.fails('全pathが /{slug} 形式(階層なし)') // ^/[^/]+$ で検証
})
it.fails の挙動:
- 現在: アサーションが失敗する →
it.failsとして正常(テストスイートはPASS) - 移行後: アサーションが成功する →
it.failsのままだとテストスイートがFAIL
移行完了時: it.fails → it に変更してテスト実行。全PASSで完了確認。
移行完了条件
移行完了とみなす条件:
- 全対象記事(318件)のpathが新URL体系に更新済み
- 旧URL→新URLの301リダイレクトが全件動作確認済み
- 内部リンクが全て新URLに更新済み(日付パターンの残存ゼロ)
- 画像の相対パスが全て絶対パスに変換済み(24ファイル)
- 404エラーがゼロ(内部リンク、外部リンク、画像全て)
- Google Search Consoleでエラーゼロ
- サイトマップが新URLで生成されている
- canonical URLが新URLを指している
- OGP画像URLが新URLで動作
次のアクション
ステータス: 設計確定済み、実装待ち
- Step 0: テストコード実装(
tests/url-migration.test.ts) - Step 0.5: 非公開メモ(18件)を
drafts/へ移動 - Step 1: 重複slug統合(5種類12ファイル)+ 予約語衝突解消(1件: index.md)
- Step 2: path一括更新スクリプト作成・実行(318件)
- Step 3:
_redirectsファイル生成(318件) - Step 4: 画像の相対パス修正(24ファイル)
- Step 5: 内部リンク更新
- Step 6: 動作確認・404チェック
- Google Search Console でサイトマップ再送信
備考: この文書自身(url-structure-improvement.md)も移行対象。移行後は /url-structure-improvement でアクセス可能になる。
参考
- Nuxt Content v3 では
pathフィールドでURLを完全制御可能 - ファイル構造とURLは独立しているため、ファイル移動なしでURL体系を変更可能
_redirectsファイルはCloudflare Pages標準機能(ドキュメント)