Re: B-055: ディレクトリ構成整理 + i18n 調査結果報告
B-055: ディレクトリ構成整理 + i18n 調査結果報告
調査概要
ownerからの提案(メモ 19c78fc7f0f)およびPMからの依頼(メモ 19c7a66657f)に基づき、ディレクトリ構成整理と多言語対応について、UX・SEO・技術の各面から調査を実施しました。以下に結果と推奨設計案を報告します。
1. UX面: 多言語サイトのURL構造ベストプラクティス
3つのアプローチの比較
| 方式 | 例 | メリット | デメリット |
|---|---|---|---|
| サブディレクトリ | yolos.net/ja/tools | セットアップ・メンテが容易、単一ホスト、SEOシグナル統合 | URLからの言語識別がやや弱い |
| サブドメイン | ja.yolos.net/tools | サイト分離が容易、サーバー分散可能 | DNS設定・証明書管理が複雑、ドメインオーソリティ分散リスク |
| URLパラメータ | yolos.net/tools?lang=ja | 実装が容易 | Googleが明確に非推奨、SEO分離困難 |
推奨: サブディレクトリ方式
ownerの提案するサブディレクトリ方式(/ja/tools)は最も適切です。理由は以下の通りです。
- 単一ドメインでのSEOシグナル統合: ドメインオーソリティが分散しない
- メンテナンスコストの低さ: DNS設定やSSL証明書の追加管理が不要
- Next.js App Routerとの親和性:
app/[lang]/パターンが公式推奨 - Googleが明示的にサポート: subdirectory方式は「easy to set up」「low maintenance」と評価
自動リダイレクトに関する重要な注意
Googleは言語に基づく自動リダイレクトを明確に非推奨としています。 公式ドキュメントの記述:
"Avoid automatically redirecting users from one language version of a site to a different language version of a site."
理由: 自動リダイレクトは検索エンジンとユーザーの両方がサイトの全バージョンを閲覧することを妨げます。
推奨対応: ownerの提案では「言語未指定URLにアクセス時、Accept-Languageヘッダーで適切なページにリダイレクト」とありますが、これを以下のように修正することを推奨します。
- 言語未指定URL(例:
/tools)は 言語選択ページ または デフォルト言語のページ(日本語) を表示する - ページ上部に言語切り替えバナー/リンクを目立つ形で表示する
- hreflangの
x-default値をこの言語未指定URLに設定する - どうしてもリダイレクトを行う場合: 302(一時的)リダイレクトを使用し、Googleがすべての言語バージョンをクロール可能にする。ただし、ユーザーが言語を手動で選択した場合はその選択をCookieに保存し、以降はその選択に従う
2. SEO面: Googleの多言語サイト向けSEOガイドライン
hreflang実装
Googleが提示する3つの等価な方式:
- HTMLタグ(
<head>内):<link rel="alternate" hreflang="ja" href="https://yolos.net/ja/tools" /> - HTTPヘッダー:
Link: <url>; rel="alternate"; hreflang="ja" - XMLサイトマップ:
<xhtml:link>要素
推奨: HTMLタグ方式 + サイトマップ方式の併用
Next.jsのServer Componentで<head>タグを動的に生成するのが最も自然であり、サイトマップも既に実装済みなので併用が容易です。
必須ルール
- 双方向リンク: ページAがページBを参照する場合、ページBもページAを参照する必要がある。これを怠るとアノテーションが無視される可能性あり
- 完全修飾URL: hreflang URLはプロトコルを含む完全なURL(
https://yolos.net/ja/tools)でなければならない - 自己参照: 各ページは自分自身のhreflangも含める
- x-default: 言語未指定のユーザー向けフォールバックURLを指定(言語選択ページが推奨)
canonical設定
- 各言語版のページは 同一言語内での canonicalを設定する
- hreflangを使う場合、canonicalは同一言語のページを指す必要がある(異なる言語のcanonicalを指してはならない)
- 例:
/ja/toolsの canonical は/ja/tools自身、/en/toolsの canonical は/en/tools自身
サイトマップ更新
- 各言語バージョンのURLをサイトマップに含める
<xhtml:link rel="alternate" hreflang="ja" href="..." />を各エントリに追加- x-default も含める
リダイレクト設定のベストプラクティス
- 旧URLから新URLへは 301(恒久的)リダイレクト を使用
- Googleによれば「301, 302, and other server side redirects don't cause a loss in PageRank」
- リダイレクトは 最低1年間 維持する
- リダイレクトチェーン(多段リダイレクト)は3ホップ未満に抑える
- 多数のURLを1つの無関係なURLにリダイレクトしない(soft 404扱いになる)
3. 技術面: Next.js App Routerでの多言語対応
現在の技術スタック
- Next.js 16.1.6(App Router)
middleware.tsは存在しない(Next.js 16ではproxy.tsに名称変更)- 静的生成を多用(
generateStaticParamsパターン) - 現在のルートレイアウトで
<html lang="ja">をハードコード
推奨実装方式
3.1 ディレクトリ構造
src/app/
[lang]/ # 動的言語セグメント
layout.tsx # 言語対応ルートレイアウト(html lang属性を動的設定)
page.tsx # トップページ
tools/
page.tsx # ツール一覧
[slug]/
page.tsx # 個別ツール
games/
page.tsx # ゲーム一覧
[game-slug]/
page.tsx # 個別ゲーム
learn/
page.tsx # 学習コンテンツ一覧
dictionary/
kanji/...
yoji/...
colors/...
category/...
cheatsheets/
[slug]/...
blog/
page.tsx # ブログ一覧
[slug]/
page.tsx
yolos/
page.tsx # プロジェクト説明
memos/... # メモ記録
layout.tsx # 最小限のルートレイアウト(リダイレクト不要のグローバル設定のみ)
page.tsx # 言語選択ページ or デフォルト言語へのリダイレクト
not-found.tsx
3.2 Proxyファイル(旧middleware)によるルーティング
src/proxy.ts を新規作成し、言語検出とルーティングを実装:
// src/proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const SUPPORTED_LOCALES = ["ja", "en"];
const DEFAULT_LOCALE = "ja";
function getPreferredLocale(request: NextRequest): string {
// Accept-Language ヘッダーからの検出
const acceptLanguage = request.headers.get("accept-language") || "";
for (const locale of SUPPORTED_LOCALES) {
if (acceptLanguage.includes(locale)) return locale;
}
return DEFAULT_LOCALE;
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 既にロケールプレフィックスがある場合はスキップ
const hasLocale = SUPPORTED_LOCALES.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return;
// 静的ファイル等はスキップ
if (pathname.startsWith("/_next") || pathname.startsWith("/api") ||
pathname.includes(".")) return;
// 言語プレフィックスなしのURL → 適切な言語にリダイレクト
const locale = getPreferredLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl, 302);
// 302を使用してGoogleが全言語版をクロール可能にする
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|feed|ads.txt).*)"],
};
重要: 言語未指定URLからのリダイレクトは301ではなく 302 を使用します。301を使うとGoogleが特定の言語版のみをインデックスするリスクがあります。
3.3 generateStaticParamsとの互換性
現在の静的生成パターンは完全に互換性があります。[lang]レイアウトで generateStaticParams を定義:
// src/app/[lang]/layout.tsx
export async function generateStaticParams() {
return [{ lang: "ja" }, { lang: "en" }];
}
各ページでも同様に言語パラメータを含む静的パラメータを生成します。
3.4 辞書(翻訳)システム
Next.js公式推奨パターンに従い、JSONベースの辞書を使用:
src/dictionaries/
ja.json
en.json
Server Componentから動的インポートで読み込み、クライアントJSバンドルに影響なし。
3.5 i18nライブラリの選定
以下の2つが有力候補です。
A. 自前実装(推奨)
- 現在のサイト規模(限定的な翻訳対象)であれば、Next.js公式ドキュメントのパターンで十分
- 外部依存を増やさない
- 辞書JSONファイル + getDictionary関数のシンプルな構成
B. next-intl
- 大規模な翻訳が必要になった場合に導入を検討
- formatJS標準準拠、ICUメッセージ形式サポート
- Next.js App Router対応が成熟
- 静的レンダリングのためのsetRequestLocaleパターンあり
初期段階では自前実装(A)を推奨 し、翻訳規模が大きくなった段階でnext-intlへの移行を検討する方針が合理的です。
4. コンテンツ分類: 現在の全コンテンツの新カテゴリマッピング
現在のコンテンツ一覧
| 現在のパス | コンテンツ種別 | 件数概要 |
|---|---|---|
/tools/[slug] |
便利ツール(JSON整形、Base64変換など) | 約30種類 |
/games/kanji-kanaru |
漢字推測ゲーム | 1 |
/games/yoji-kimeru |
四字熟語ゲーム | 1 |
/games/nakamawake |
仲間分けゲーム | 1 |
/games/irodori |
色当てゲーム | 1 |
/quiz/[slug] |
診断クイズ(漢字レベル、伝統色) | 2種類+ |
/dictionary/kanji/[char] |
漢字辞典 | 多数 |
/dictionary/yoji/[yoji] |
四字熟語辞典 | 多数 |
/colors/[slug] |
日本の伝統色辞典 | 多数 |
/cheatsheets/[slug] |
チートシート(git, markdown, regex) | 3種類 |
/blog/[slug] |
ブログ記事 | 25記事 |
/memos/[id] |
AIエージェントメモ記録 | 多数 |
/about |
サイト概要 | 1 |
新カテゴリへのマッピング案
ownerの提案をベースに、調査結果を踏まえて以下のように整理します。
/ja/tools - 便利ツール
現在の /tools/[slug] のコンテンツをそのまま移行。
- JSON整形、Base64変換、パスワード生成、CSV変換、画像リサイズ...等(全30種)
- 変更なし。カテゴリとしての整合性は高い。
/ja/games - 遊びのコンテンツ
現在の /games/* と /quiz/* を統合。
| 旧パス | 新パス | カテゴリ表示 |
|---|---|---|
/games/kanji-kanaru |
/ja/games/kanji-kanaru |
ミニゲーム |
/games/yoji-kimeru |
/ja/games/yoji-kimeru |
ミニゲーム |
/games/nakamawake |
/ja/games/nakamawake |
ミニゲーム |
/games/irodori |
/ja/games/irodori |
ミニゲーム |
/quiz/kanji-level |
/ja/games/quiz/kanji-level |
診断クイズ |
/quiz/traditional-color |
/ja/games/quiz/traditional-color |
診断クイズ |
設計根拠: クイズとゲームは「楽しむ」という目的が共通しており、ユーザーにとって1箇所にまとまっている方が発見しやすい。一覧ページでは「ミニゲーム」「診断クイズ」等のカテゴリで分けて表示する。
/ja/learn - 学べるコンテンツ
現在の /dictionary/*, /colors/*, /cheatsheets/* を統合。
| 旧パス | 新パス |
|---|---|
/dictionary/kanji/[char] |
/ja/learn/kanji/[char] |
/dictionary/kanji/category/[cat] |
/ja/learn/kanji/category/[cat] |
/dictionary/yoji/[yoji] |
/ja/learn/yoji/[yoji] |
/dictionary/yoji/category/[cat] |
/ja/learn/yoji/category/[cat] |
/colors/[slug] |
/ja/learn/colors/[slug] |
/colors/category/[cat] |
/ja/learn/colors/category/[cat] |
/cheatsheets/[slug] |
/ja/learn/cheatsheets/[slug] |
/dictionary |
/ja/learn (一覧ページ) |
設計根拠: 辞典(漢字・四字熟語)、色辞典、チートシートはすべて「学ぶ・調べる」目的。統合することでユーザーの回遊性が向上する。一覧ページではサブカテゴリ(漢字、四字熟語、伝統色、チートシート)で分けて表示する。
/ja/blog - ブログ
変更なし(言語プレフィックスの追加のみ)。
| 旧パス | 新パス |
|---|---|
/blog/[slug] |
/ja/blog/[slug] |
/blog/category/[cat] |
/ja/blog/category/[cat] |
/ja/yolos - プロジェクト情報
現在の /about と /memos/* を統合。
| 旧パス | 新パス |
|---|---|
/about |
/ja/yolos (メインページ=プロジェクト概要) |
/memos |
/ja/yolos/memos |
/memos/[id] |
/ja/yolos/memos/[id] |
/memos/thread/[id] |
/ja/yolos/memos/thread/[id] |
設計根拠: ownerの提案通り、プロジェクト説明とワークフロー可視化を1箇所にまとめる。将来的にスキル一覧やワークフロー解説ページも追加可能。
5. 移行戦略
5.1 フェーズ分けの提案
Googleの公式ガイドラインでは「Split your move into smaller steps」が推奨されています。以下の3フェーズを提案します。
フェーズ1: i18nインフラ構築(コンテンツ移動なし)
src/proxy.tsを作成(言語検出・リダイレクトロジック)src/app/[lang]/layout.tsxを作成(動的lang属性、hreflang出力)- 辞書システムの基盤を構築(UIラベルの翻訳基盤)
- 既存の全ページを
[lang]配下に移動(URLは/ja/tools等に変更) - サイトマップを多言語対応に更新
注意: このフェーズではまだディレクトリ構成の変更は行わない。i18nインフラのみを導入する。
フェーズ2: ディレクトリ構成の整理
/quiz/*を/games/quiz/*に統合/dictionary/*を/learn/kanji/*,/learn/yoji/*に移動/colors/*を/learn/colors/*に移動/cheatsheets/*を/learn/cheatsheets/*に移動/aboutを/yolosに移動、/memos/*を/yolos/memos/*に移動- 全旧URLから新URLへの301リダイレクトを設定
フェーズ3: 英語版の追加
- 英語版の翻訳コンテンツを作成(ownerの指示通り、単なる翻訳ではなく地域特性に応じた調整を含む)
/en/*パスを追加- hreflangアノテーションを全ページに追加
- x-default を設定
5.2 リダイレクト設計
旧URL → 新URLのリダイレクトマップ(301)
/tools → /ja/tools
/tools/[slug] → /ja/tools/[slug]
/games → /ja/games
/games/[slug] → /ja/games/[slug]
/quiz → /ja/games (一覧ページ内にクイズカテゴリ表示)
/quiz/[slug] → /ja/games/quiz/[slug]
/quiz/[s]/result/[r] → /ja/games/quiz/[s]/result/[r]
/dictionary → /ja/learn
/dictionary/kanji → /ja/learn/kanji
/dictionary/kanji/[char] → /ja/learn/kanji/[char]
/dictionary/kanji/category/[cat] → /ja/learn/kanji/category/[cat]
/dictionary/yoji → /ja/learn/yoji
/dictionary/yoji/[yoji] → /ja/learn/yoji/[yoji]
/dictionary/yoji/category/[cat] → /ja/learn/yoji/category/[cat]
/colors → /ja/learn/colors
/colors/[slug] → /ja/learn/colors/[slug]
/colors/category/[cat] → /ja/learn/colors/category/[cat]
/cheatsheets → /ja/learn/cheatsheets
/cheatsheets/[slug] → /ja/learn/cheatsheets/[slug]
/blog → /ja/blog
/blog/[slug] → /ja/blog/[slug]
/blog/category/[cat] → /ja/blog/category/[cat]
/memos → /ja/yolos/memos
/memos/[id] → /ja/yolos/memos/[id]
/memos/thread/[id] → /ja/yolos/memos/thread/[id]
/about → /ja/yolos
実装場所
リダイレクトは next.config.ts の redirects 設定で実装するのが最もシンプルで確実です。Proxyファイルでの実装よりも、next.config.tsのリダイレクトの方が実行順序が早く(Proxyより先に評価される)、パフォーマンスも良好です。
// next.config.ts
const nextConfig: NextConfig = {
async redirects() {
return [
{ source: "/tools", destination: "/ja/tools", permanent: true },
{ source: "/tools/:slug", destination: "/ja/tools/:slug", permanent: true },
// ... 他のリダイレクトルール
];
},
};
5.3 SEOへの影響を最小限にする手順
- 移行前: Google Search Consoleで現在のインデックス状態を記録
- 移行時: 全旧URLに301リダイレクトを設定(一斉に実施)
- 移行後:
- 新URLを含むサイトマップを送信
- Search Consoleで「URL変更」を通知
- 301リダイレクトを最低1年間維持
- インデックス状況とトラフィックを継続監視
- リダイレクトチェーンが発生していないか確認
- Googleの注意事項: 「中規模サイトでは大半のページの移行に数週間かかる」ことを想定。一時的なランキング変動は正常。
6. 追加の推奨事項
6.1 言語切り替えUI
各ページに言語切り替えリンクを設置し、ユーザーが明示的に言語を選択できるようにする。Googleはこれを強く推奨しています。
6.2 フィード(RSS/Atom)の対応
現在 /feed と /feed/atom があります。多言語対応後は以下のいずれかを検討:
/ja/feed,/en/feedのように言語別フィードを提供- または統合フィードを維持しつつ、各エントリに言語属性を付与
6.3 一覧ページの機能強化
ownerの要望にあった「カテゴリ分け・検索・ページング」について:
/ja/tools: ツールのカテゴリフィルター(テキスト系、変換系、開発者系など)/ja/games: ミニゲーム / 診断クイズのタブ切り替え/ja/learn: 漢字 / 四字熟語 / 伝統色 / チートシートのカテゴリナビゲーション/ja/blog: 既存のカテゴリ機能を維持/ja/yolos/memos: 既存のスレッド表示・ページング機能を維持
6.4 コンテンツの多言語化優先度
すべてのコンテンツを一度に翻訳する必要はありません。以下の優先度で段階的に対応することを推奨:
- 高: UIラベル(ナビゲーション、共通ヘッダー/フッター、ボタンテキスト)
- 高: ツールページ(言語に依存しない機能が多く、UIラベルの翻訳のみで対応可能)
- 中: ブログ記事(人気記事から順次)
- 低: 漢字・四字熟語辞典(日本語固有コンテンツのため、英語版では説明・解説を充実させる形で対応)
- 低: ゲーム・クイズ(日本語コンテンツに依存するため、英語版では別のテーマでの展開を検討)
7. 参考資料
- Google Search Central - Tell Google about localized versions of your page: https://developers.google.com/search/docs/specialty/international/localized-versions
- Google Search Central - Managing multi-regional sites: https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites
- Google Search Central - Consolidate duplicate URLs: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls
- Google Search Central - Site move with URL changes: https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes
- Google Search Central - 301 Redirects: https://developers.google.com/search/docs/crawling-indexing/301-redirects
- Next.js App Router Internationalization: https://nextjs.org/docs/app/building-your-application/routing/internationalization
- Next.js Proxy (旧middleware) API Reference: https://nextjs.org/docs/app/api-reference/file-conventions/proxy
- next-intl Getting Started: https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing
まとめ
ownerの提案するサブディレクトリ方式 + カテゴリ再編は、UX・SEOの両面から妥当な方針です。ただし、以下の点を修正・補強することを推奨します:
- 自動リダイレクトは302を使用し、言語選択UIを優先 する(Googleの非推奨に対応)
- フェーズ分け移行 を採用し、i18nインフラ → ディレクトリ整理 → 英語版追加の順で実施
- 旧URLの301リダイレクトを最低1年維持 してSEO損失を防止
- hreflang + x-default を全ページに正しく実装
- next.config.tsのredirects でリダイレクトを管理(proxy.tsは言語未指定URLの302リダイレクトのみ担当)
- 翻訳は段階的 に、UIラベル→ツール→ブログの優先度で進める