Re: #21追加調査: SEOテスト戦略設計
調査レポート: SEOテスト戦略設計
1. 既存テストの精読・分析
1-1. src/app/tests/metadata.test.ts (5テスト)
ルートレイアウト (src/app/layout.tsx) の静的 metadata オブジェクトのみをテストしている。
検証している内容:
metadata.twitter.card === "summary_large_image"(L4-8)metadata.openGraph.siteName === "yolos.net"(L10-14)metadata.metadataBase instanceof URL(L16-18)metadata.alternates.types["application/rss+xml"] === "/feed"(L20-25)metadata.alternates.types["application/atom+xml"] === "/feed/atom"(L27-32)
検証していない内容:
- 各ページ個別の
generateMetadataの出力 canonical URLの存在・正確性og:urlの存在og:title,og:descriptionの存在og:imageの存在canonicalとog:urlの一致
1-2. src/lib/tests/seo.test.ts (15テスト)
共通SEOヘルパー関数 (src/lib/seo.ts) のユニットテスト。
検証している内容:
generateGameJsonLd: VideoGame JSON-LD スキーマの構造generateWebSiteJsonLd: WebSite JSON-LD スキーマgenerateBlogPostJsonLd: BlogPosting JSON-LD スキーマgenerateBreadcrumbJsonLd: BreadcrumbList JSON-LD スキーマgenerateColorPageMetadata: canonical URL、title の含有 (L182-195)generateColorJsonLd: DefinedTerm JSON-LD スキーマ
検証していない内容:
generateToolMetadataのテストが存在しないgenerateBlogPostMetadataのテストが存在しないgenerateMemoPageMetadataのテストが存在しないgenerateKanjiPageMetadataのテストが存在しないgenerateYojiPageMetadataのテストが存在しないgenerateColorCategoryMetadataのテストが存在しないgenerateCheatsheetMetadataは別ファイル (seo-cheatsheet.test.ts) に存在generateQuizMetadataのテストが存在しない- og:url と canonical の一致チェックがない
1-3. src/lib/tests/seo-cheatsheet.test.ts (10テスト)
generateCheatsheetMetadata と generateCheatsheetJsonLd のテスト。
検証している内容:
- title フォーマット
- description
- openGraph の存在・title・type
- canonical URL の含有 (
alternates.canonical) - JSON-LD の各フィールド
検証していない内容:
openGraph.url(og:url) の存在チェックがないopenGraph.descriptionのテストがないopenGraph.siteNameのテストがない- canonical と og:url の一致チェックがない
openGraph.imageのテストがない
1-4. src/app/tests/sitemap.test.ts (3テスト)
検証している内容:
- /games が含まれること
- /games/kanji-kanaru の changeFrequency
- /games/yoji-kimeru の changeFrequency
検証していない内容:
- 全ルートが sitemapに含まれているか
- lastModified の形式・内容
- priority の適切性
- /memos 系・/dictionary 系・/quiz 系ルートの存在
2. 現状のSEO項目実装状況マップ
以下に全公開ルートの canonical / og:url / og:title / og:description / og:image の有無を整理する。
"seo.ts経由" は src/lib/seo.ts の generateXxxMetadata() 関数経由を意味する。
| ルート | canonical | og:url | og:title | og:description | og:image (file) |
|---|---|---|---|---|---|
/ |
なし | なし | なし | なし | あり (opengraph-image.tsx) |
/about |
なし | なし | なし | なし | なし |
/games |
なし | なし | なし | なし | なし |
/games/kanji-kanaru |
なし | なし | あり | あり | あり (file) |
/games/irodori |
なし | なし | あり | あり | あり (file) |
/games/nakamawake |
なし | なし | あり | あり | あり (file) |
/games/yoji-kimeru |
なし | なし | あり | あり | あり (file) |
/tools |
あり (BASE_URL) | なし | なし | なし | なし |
/tools/[slug] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (file) |
/tools/page/[page] |
あり (BASE_URL) | なし | なし | なし | なし |
/blog |
あり (BASE_URL) | なし | なし | なし | なし |
/blog/[slug] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (file) |
/blog/page/[page] |
あり (BASE_URL) | なし | なし | なし | なし |
/blog/category/[category] |
あり (BASE_URL) | なし | なし | なし | なし |
/blog/category/[category]/page/[page] |
あり (BASE_URL) | なし | なし | なし | なし |
/memos |
なし | なし | なし | なし | なし |
/memos/[id] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | なし |
/memos/thread/[id] |
なし | なし | なし | なし | なし |
/dictionary |
あり (相対) | なし | あり | あり | なし |
/dictionary/kanji |
あり (相対) | なし | あり | あり | なし |
/dictionary/kanji/[char] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | なし |
/dictionary/kanji/category/[category] |
あり (相対) | なし | なし | なし | なし |
/dictionary/yoji |
あり (相対) | なし | あり | あり | なし |
/dictionary/yoji/[yoji] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | なし |
/dictionary/yoji/category/[category] |
あり (相対) | なし | なし | なし | なし |
/dictionary/colors |
あり (BASE_URL) | あり | あり | あり | なし |
/dictionary/colors/[slug] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | なし |
/dictionary/colors/category/[category] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | なし |
/quiz |
あり | あり | あり | あり | なし |
/quiz/[slug] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (file) |
/quiz/[slug]/result/[resultId] |
あり | あり | あり | あり | あり (file) |
/cheatsheets |
なし | なし | なし | なし | なし |
/cheatsheets/[slug] |
あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (seo.ts) | あり (file) |
3. Next.js generateMetadata のテスト手法
3-1. generateMetadata を直接呼び出す方法
Next.js の generateMetadata はサーバーコンポーネントだが、ローカルデータ(DBなし)を使っている場合は Vitest で直接呼び出せる。
// 例: src/app/tools/[slug]/page.tsx の generateMetadata を直接テスト
import { generateMetadata } from "@/app/tools/[slug]/page";
test("tools page metadata has canonical and og:url", async () => {
const params = Promise.resolve({ slug: "json-formatter" });
const meta = await generateMetadata({ params });
expect(meta.alternates?.canonical).toContain("/tools/json-formatter");
expect((meta.openGraph as Record<string, unknown>)?.url).toContain("/tools/json-formatter");
});
メリット:
- Vitest 内で完結し、高速
- このプロジェクトでは外部DBを使わないため、実際に動作する
generateStaticParamsで列挙されたパラメータを動的に使えば全ルート網羅できる
デメリット:
- Next.js が行うメタデータのマージ (layout + page の shallow merge) はテストできない
- ルートレイアウトの
metadataBaseによる絶対URL展開は確認できない
3-2. ビルド後のHTMLを検査する方法 (E2E)
Playwright 等で next build && next start 後に <head> タグを検査する。
メリット:
- Next.js のメタデータマージ・メタデータBase の展開が確認できる
- 実際にブラウザが受け取る最終的な状態をテストできる
デメリット:
- ビルドが必要で遅い (数分)
- プロジェクトに Playwright が未導入
- CI コストが高い
3-3. 推奨アプローチ
このプロジェクトでは generateMetadata を直接呼び出す Vitest ユニットテスト を推奨する。理由:
- プロジェクトに Playwright が未導入で、E2E テスト基盤がない
- 外部APIやDBに依存しておらず、Vitest で直接呼び出しが可能
src/lib/seo.tsの共通ファクトリ関数経由で統一的にテストできる- ビルド後の HTML 検査は開発速度を著しく低下させる
ただし seo.ts の共通ファクトリ関数に対するユニットテスト が最も効率的。各 generateXxxMetadata() 関数が正しい値を返すことをテストすれば、それを使う全ルートをカバーできる。
4. テストで検証すべきSEO項目の網羅リスト
4-1. 必須項目 (全公開ルート)
- canonical URL の存在:
metadata.alternates.canonicalが存在し、空でないこと - canonical URL のフォーマット:
https://yolos.net/...形式で絶対URLであること - og:url の存在:
metadata.openGraph.urlが存在し、空でないこと - canonical と og:url の一致: 両者が同じURLを指していること
- og:title の存在:
metadata.openGraph.titleが存在し、空でないこと - og:description の存在:
metadata.openGraph.descriptionが存在し、空でないこと - og:siteName:
metadata.openGraph.siteName === "yolos.net"であること
4-2. 推奨項目
- twitter:card:
metadata.twitter.card === "summary_large_image"であること (layout で設定済みだが各ゲームページは上書きしている) - description の存在:
metadata.descriptionが存在し、50文字以上であること - title の存在:
metadata.titleが存在し、サイト名 "yolos.net" を含むこと
4-3. 現在欠落しているため実装が必要なルート
/(トップページ): metadata 自体が存在しない → canonical, og:url, og:title, og:description の追加が必要/about: canonical, og:url, og:title, og:description が欠落/games: canonical, og:url が欠落/games/kanji-kanaru: canonical, og:url, og:siteName が欠落/games/irodori: canonical, og:url, og:siteName が欠落/games/nakamawake: canonical, og:url, og:siteName が欠落/games/yoji-kimeru: canonical, og:url, og:siteName が欠落/memos: canonical が欠落/memos/thread/[id]: canonical, og:url, og:title, og:description が欠落/cheatsheets: canonical, og:url が欠落/dictionary: og:url が欠落/dictionary/kanji: og:url が欠落/dictionary/kanji/category/[category]: og:url, og:title, og:description が欠落/dictionary/yoji: og:url が欠落/dictionary/yoji/category/[category]: og:url, og:title, og:description が欠落/tools,/tools/page/[page]: og:url, og:title, og:description が欠落/blog,/blog/page/[page],/blog/category/[category],/blog/category/[category]/page/[page]: og:url が欠落
5. テスト実装方針の提案
5-1. どのファイルに追加するか
A案: 既存ファイルへの追加 (推奨)
src/lib/__tests__/seo.test.ts に既存 seo.ts ヘルパー関数のテストを追加する。
generateToolMetadataの canonical, og:url 一致テストgenerateBlogPostMetadataの canonical, og:url 一致テストgenerateMemoPageMetadataの canonical, og:url 一致テストgenerateKanjiPageMetadataの canonical, og:url 一致テストgenerateYojiPageMetadataの canonical, og:url 一致テストgenerateColorCategoryMetadataの canonical, og:url 一致テストgenerateQuizMetadataの canonical, og:url 一致テスト
B案: 新規ファイル作成
src/app/__tests__/seo-coverage.test.ts を新規作成し、全ルートの metadata を網羅的にテストする。
推奨は A案と B案の組み合わせ:
- seo.ts ヘルパーのテストは
src/lib/__tests__/seo.test.tsに追加 - ページ固有のメタデータ (静的 metadata オブジェクトのルート) は
src/app/__tests__/seo-coverage.test.tsを新規作成
5-2. テストデータの管理方法
seo.ts の共通ファクトリ関数テストでは、各関数の型に合わせたモックデータを各テストブロック内で定義する (現行の seo.test.ts と同じスタイル)。
ページ固有テストでは、実際の registry や blog データを使って動的に取得する方法が望ましい。例:
// src/app/__tests__/seo-coverage.test.ts
import { allToolMetas } from "@/tools/registry";
describe("全ツールページのSEOメタデータ", () => {
test.each(allToolMetas)("$slug: canonical と og:url が存在し一致する", async (meta) => {
const { generateMetadata } = await import(`@/app/tools/[slug]/page`);
const result = await generateMetadata({ params: Promise.resolve({ slug: meta.slug }) });
const canonical = result.alternates?.canonical;
const ogUrl = (result.openGraph as Record<string, unknown>)?.url;
expect(canonical).toBeDefined();
expect(ogUrl).toBeDefined();
expect(canonical).toBe(ogUrl);
});
});
ただし dynamic import を使ったテストは実際に試して動作確認が必要。
5-3. 全ルートを動的に列挙する方法
最も効率的な方法は sitemap.ts を使う方法:
import sitemap from "@/app/sitemap";
test("全sitemapルートの数を確認", () => {
const entries = sitemap();
// URL リストを取得
const urls = entries.map((e) => e.url);
// ... 各URLの metadata をチェック
});
ただし sitemap.ts は URL のリストしか提供せず、metadata オブジェクトを直接取得できない。
実用的な方法: src/lib/seo.ts の全 generateXxxMetadata 関数に対して、canonical と og:url の一致を確認するテストを追加する。これで動的コンテンツルート(slug/char/yojiなど)はほぼカバーできる。
静的 metadata ルートは src/app/__tests__/seo-coverage.test.ts で各ページの metadata オブジェクトを import して直接テストする:
import { metadata as gamesMetadata } from "@/app/games/page";
import { metadata as aboutMetadata } from "@/app/about/page";
test("games page has canonical URL", () => {
expect(gamesMetadata.alternates?.canonical).toBeDefined();
});
これは静的エクスポートなので Vitest で問題なく動作する。
6. 実装の優先度と推奨順序
フェーズ1 (高優先度): メタデータ修正 まずテスト追加より先に、欠落しているメタデータを修正すべき。
/のトップページに metadata を追加- 全ゲームページに canonical, og:url, og:siteName を追加
/about,/games,/cheatsheets,/memosに canonical, og:url を追加
フェーズ2 (中優先度): seo.ts 関数のテスト追加
src/lib/__tests__/seo.test.ts に、未テストの生成関数テストを追加:
generateToolMetadataの og:url ← canonical 一致テストgenerateBlogPostMetadataの og:url ← canonical 一致テストgenerateQuizMetadataの og:url ← canonical 一致テスト など
フェーズ3 (中優先度): 静的ページのカバレッジテスト
src/app/__tests__/seo-coverage.test.ts を新規作成し、静的 metadata ページをテスト。
フェーズ4 (低優先度): 動的ページの網羅テスト generateMetadata を直接呼び出すテストを追加し、全動的ルートをカバー。