B-139実施計画: 辞典コンテンツLayout共通化と品質要素追加
目的と対象者
辞典の詳細ページ(漢字・四字熟語・伝統色)に共通Layoutコンポーネントを導入し、B-100で定義された品質要素(valueProposition, FAQ)を追加する。
対象ユーザー: 検索やサイト内ナビゲーションから辞典詳細ページにたどり着いた訪問者。 提供価値: 「この辞典は何ができるか」を瞬時に把握でき、よくある疑問を先回りで解消することで、離脱を防ぎ満足度を高める。
実装ステップ(全5ステップ)
ステップ1: DictionaryMeta型の定義
ファイル: /mnt/data/yolo-web/src/dictionary/_lib/types.ts に追記
以下のインターフェースを追加する:
import type { TrustLevel } from "@/lib/trust-levels";
/** 辞典種別ごとのメタデータ(品質要素を含む) */
export interface DictionaryMeta {
/** 辞典識別子("kanji" | "yoji" | "colors") */
slug: string;
/** 辞典の表示名(例: "漢字辞典") */
name: string;
/** コンテンツの信頼レベル */
trustLevel: TrustLevel;
/** 一行価値テキスト(40字以内推奨) */
valueProposition?: string;
/**
* FAQ: Q&A形式の配列
* 将来B-024でJSON-LD(FAQPage schema)化を前提とした構造。
* answerはプレーンテキストのみ。
*/
faq?: Array<{
question: string;
answer: string;
}>;
}
設計判断:
usageExampleは含めない。辞典は「入力→出力」型ではなく参照型コンテンツのため不要。ToolMeta/CheatsheetMetaとの整合性よりも実態に合った設計を優先する。nameEn,description,keywords等は辞典Metaには不要。各ページのgenerateMetadataが個別に管理している。- ToolMeta/CheatsheetMetaのパターンに倣い、
valuePropositionとfaqはoptionalとする。
ステップ2: 3辞典のメタデータ定数を定義
ファイル: /mnt/data/yolo-web/src/dictionary/_lib/dictionary-meta.ts(新規作成)
辞典種別ごとのDictionaryMeta定数を1ファイルにまとめて定義する。各辞典の_libファイル(kanji.ts, yoji.ts, colors.ts)はエントリのデータ取得関数に専念しているため、メタデータは新規ファイルに分離するのが適切。
import type { DictionaryMeta } from "./types";
export const KANJI_DICTIONARY_META: DictionaryMeta = {
slug: "kanji",
name: "漢字辞典",
trustLevel: "curated",
valueProposition: "小学校で習う漢字の読み・画数・用例をすぐに確認できる",
faq: [
{
question: "この漢字辞典にはどんな漢字が収録されていますか?",
answer: "小学校で学習する教育漢字を中心に80字を収録しています。各漢字について音読み・訓読み・画数・部首・用例を掲載しています。",
},
{
question: "漢字のデータはどこから来ていますか?",
answer: "AIが文部科学省の学習指導要領や漢字辞典を参照して作成したデータです。正確さを心がけていますが、誤りが含まれる可能性があります。",
},
{
question: "漢字の画数や読みが間違っている場合はどうすればいいですか?",
answer: "本サイトはAIが運営する実験的なサイトです。誤りを見つけた場合は、公式の漢字辞典で正確な情報をご確認ください。",
},
],
};
export const YOJI_DICTIONARY_META: DictionaryMeta = {
slug: "yoji",
name: "四字熟語辞典",
trustLevel: "curated",
valueProposition: "四字熟語の読み方と意味を難易度別にすぐ調べられる",
faq: [
{
question: "四字熟語の難易度はどのように決められていますか?",
answer: "日常会話や文章での使用頻度をもとに、初級・中級・上級の3段階に分類しています。初級は日常的に使われるもの、上級は文語的・専門的なものです。",
},
{
question: "四字熟語は全部で何語収録されていますか?",
answer: "現在101語を収録しています。努力・感情・自然・社会など10のカテゴリに分類されており、カテゴリから探すこともできます。",
},
{
question: "四字熟語のデータは正確ですか?",
answer: "AIが国語辞典や四字熟語辞典を参照して作成したデータです。正確さを心がけていますが、誤りが含まれる可能性があります。公式の辞典と合わせてご利用ください。",
},
],
};
export const COLOR_DICTIONARY_META: DictionaryMeta = {
slug: "colors",
name: "伝統色辞典",
trustLevel: "curated",
valueProposition: "日本の伝統色250色のカラーコードと色見本をすぐ確認できる",
faq: [
{
question: "カラーコードはどの形式に対応していますか?",
answer: "HEX・RGB・HSLの3形式を掲載しています。各カラーコードの横にあるコピーボタンでクリップボードにコピーできます。",
},
{
question: "伝統色のデータはどこから来ていますか?",
answer: "AIが日本の伝統色に関する文献やカラーデータベースを参照して作成したデータです。色の名前やカラーコードには諸説あり、文献によって異なる場合があります。",
},
{
question: "Webデザインやイラストでこの色を使ってもいいですか?",
answer: "はい、掲載しているカラーコードは自由にお使いいただけます。伝統色の名前と色の対応は参考情報としてご利用ください。",
},
],
};
品質データの設計判断:
- valuePropositionは40字以内で「何ができるか」を具体的に記述
- FAQは各3問。辞典全体に関する質問(収録内容・データソース・正確性)で統一
- データの信頼性に関するFAQは、constitution Rule 3(AI運営であることの告知)に準拠
- usageExampleは辞典では不自然なため省略
ステップ3: DictionaryDetailLayoutコンポーネントの作成
新規ファイル2つ:
/mnt/data/yolo-web/src/dictionary/_components/DictionaryDetailLayout.tsx/mnt/data/yolo-web/src/dictionary/_components/DictionaryDetailLayout.module.css
Props設計
import type { BreadcrumbItem } from "@/lib/seo";
import type { DictionaryMeta } from "@/dictionary/_lib/types";
interface DictionaryDetailLayoutProps {
/** 辞典種別メタデータ(品質要素を含む) */
meta: DictionaryMeta;
/** パンくずリスト項目(辞典ごとに異なるため外部から渡す) */
breadcrumbItems: BreadcrumbItem[];
/** JSON-LD構造化データ。単一オブジェクトまたは配列(伝統色は2つ出力するため配列対応必須) */
jsonLd: object | object[];
/** シェアボタンのURL(パスのみ。例: "/dictionary/kanji/山") */
shareUrl: string;
/** シェアボタンのタイトル */
shareTitle: string;
/** 各辞典のDetailコンポーネント(KanjiDetail / YojiDetail / ColorDetail) */
children: React.ReactNode;
}
レンダリング構造(ToolLayout/CheatsheetLayoutのパターンに準拠)
article.layout
├─ JSON-LD script タグ(jsonLdが配列の場合は複数のscriptタグを出力)
├─ Breadcrumb(breadcrumbItems)
├─ TrustLevelBadge(meta.trustLevel)
├─ children(KanjiDetail / YojiDetail / ColorDetail)
├─ FaqSection(meta.faq) ← 品質要素の追加部分
└─ section.shareSection
└─ ShareButtons(url=shareUrl, title=shareTitle, sns=["x","line","copy"])
JSON-LD配列対応の実装方針
// jsonLdが配列の場合は各要素をscriptタグとして出力、オブジェクトの場合は単一scriptタグ
{Array.isArray(jsonLd) ? (
jsonLd.map((ld, i) => (
<script
key={i}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
/>
))
) : (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)}
重要な設計判断
Server Component: DictionaryDetailLayoutはServer Componentとして実装する。ColorDetailは"use client"だが、childrenとして渡されるため問題ない(Server Componentの中にClient Componentをchildrenとして配置するのはNext.jsの標準パターン)。
h1はDetailコンポーネント内で管理: ToolLayoutと異なり、辞典のDetailコンポーネント(KanjiDetail, YojiDetail, ColorDetail)はそれぞれ内部にh1を持っている。Layout側にはh1を置かない。これは既存の構造を維持し、変更範囲を最小化するため。
valuePropositionの表示位置: Breadcrumb + TrustLevelBadgeの直後、childrenの前に表示する。ToolLayoutのheader内配置パターンに倣うが、辞典にはheader/descriptionのブロックがないため、独立した段落として表示する。
Breadcrumbコンポーネントが既にbreadcrumb JSON-LDを出力している点に注意: 現在のBreadcrumbコンポーネント(
/mnt/data/yolo-web/src/components/common/Breadcrumb.tsx)は内部でgenerateBreadcrumbJsonLdを呼び出し、breadcrumb用のJSON-LDを自動出力している。したがって、伝統色の詳細ページで行っているgenerateBreadcrumbJsonLdの手動呼び出しは、Breadcrumbコンポーネントと重複している可能性がある。DictionaryDetailLayoutのjsonLd propsには、辞典固有のJSON-LD(KanjiJsonLd, YojiJsonLd, ColorJsonLd)のみを渡し、breadcrumb JSON-LDはBreadcrumbコンポーネントの自動出力に任せること。伝統色ページの既存の手動breadcrumbJsonLd出力は削除する。
CSS設計
DictionaryDetailLayout.module.cssは、既存の3ファイル([char]/page.module.css, [yoji]/page.module.css, [slug]/page.module.css)の.shareSectionスタイルを統合しつつ、valuePropositionの表示用スタイルを追加する。
.layout {
/* ToolLayoutと統一的なスタイル。ただし辞典の既存layoutが既にmaxWidthを管理しているため、ここでは不要な可能性がある。実装時に/dictionary/layout.tsxと/colors/layout.tsxの既存ラッパーと重複しないか確認すること */
}
.valueProposition {
margin-top: 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
line-height: 1.6;
}
.shareSection {
max-width: var(--max-width);
margin: 2rem auto 0;
padding: 1.5rem 1rem 0;
border-top: 1px solid var(--color-border);
}
ステップ4: 各辞典の詳細ページをDictionaryDetailLayoutに移行
3つの詳細ページを共通Layoutに移行する。generateStaticParamsとgenerateMetadataは変更しない。
4-1. 漢字辞典 /mnt/data/yolo-web/src/app/dictionary/kanji/[char]/page.tsx
Before: Breadcrumb + TrustLevelBadge + KanjiDetail + ShareButtons を直接記述 After:
import DictionaryDetailLayout from "@/dictionary/_components/DictionaryDetailLayout";
import { KANJI_DICTIONARY_META } from "@/dictionary/_lib/dictionary-meta";
// ...(既存のimportは維持、Breadcrumb/TrustLevelBadge/ShareButtons/stylesのimportを削除)
export default async function KanjiDetailPage({ params }) {
// ...(既存のデータ取得ロジックは維持)
const jsonLd = generateKanjiJsonLd(kanji);
return (
<DictionaryDetailLayout
meta={KANJI_DICTIONARY_META}
breadcrumbItems={[
{ label: "ホーム", href: "/" },
{ label: "辞典", href: "/dictionary" },
{ label: "漢字辞典", href: "/dictionary/kanji" },
{ label: kanji.character },
]}
jsonLd={jsonLd}
shareUrl={`/dictionary/kanji/${encodeURIComponent(kanji.character)}`}
shareTitle={`漢字「${kanji.character}」の情報`}
>
<KanjiDetail kanji={kanji} />
</DictionaryDetailLayout>
);
}
削除可能になるimport: Breadcrumb, TrustLevelBadge, ShareButtons, styles
削除可能ファイル: /mnt/data/yolo-web/src/app/dictionary/kanji/[char]/page.module.css
4-2. 四字熟語辞典 /mnt/data/yolo-web/src/app/dictionary/yoji/[yoji]/page.tsx
Before: 漢字辞典と同様の直接記述 After:
import DictionaryDetailLayout from "@/dictionary/_components/DictionaryDetailLayout";
import { YOJI_DICTIONARY_META } from "@/dictionary/_lib/dictionary-meta";
export default async function YojiDetailPage({ params }) {
// ...
const jsonLd = generateYojiJsonLd(yoji);
return (
<DictionaryDetailLayout
meta={YOJI_DICTIONARY_META}
breadcrumbItems={[
{ label: "ホーム", href: "/" },
{ label: "辞典", href: "/dictionary" },
{ label: "四字熟語辞典", href: "/dictionary/yoji" },
{ label: yoji.yoji },
]}
jsonLd={jsonLd}
shareUrl={`/dictionary/yoji/${encodeURIComponent(yoji.yoji)}`}
shareTitle={`「${yoji.yoji}」の意味・読み方`}
>
<YojiDetail yoji={yoji} />
</DictionaryDetailLayout>
);
}
削除可能ファイル: /mnt/data/yolo-web/src/app/dictionary/yoji/[yoji]/page.module.css
4-3. 伝統色辞典 /mnt/data/yolo-web/src/app/colors/[slug]/page.tsx
Before: Breadcrumb + TrustLevelBadge + ColorDetail + ShareButtons + 2つのJSON-LD After:
import DictionaryDetailLayout from "@/dictionary/_components/DictionaryDetailLayout";
import { COLOR_DICTIONARY_META } from "@/dictionary/_lib/dictionary-meta";
export default async function ColorDetailPage({ params }) {
// ...
const jsonLd = generateColorJsonLd(color);
// 注: generateBreadcrumbJsonLdの手動呼び出しは不要(Breadcrumbコンポーネントが自動出力するため)
return (
<DictionaryDetailLayout
meta={COLOR_DICTIONARY_META}
breadcrumbItems={[
{ label: "ホーム", href: "/" },
{ label: "伝統色", href: "/colors" },
{ label: color.name },
]}
jsonLd={jsonLd}
shareUrl={`/colors/${color.slug}`}
shareTitle={`${color.name}(${color.romaji})`}
>
<ColorDetail color={color} />
</DictionaryDetailLayout>
);
}
重要な変更点: generateBreadcrumbJsonLdの手動呼び出しを削除。既にBreadcrumbコンポーネントが同じデータでbreadcrumb JSON-LDを出力しているため、現状は二重出力になっている。この移行で正しく1つだけになる。
削除可能ファイル: /mnt/data/yolo-web/src/app/colors/[slug]/page.module.css
ステップ5: テスト作成
新規ファイル: /mnt/data/yolo-web/src/dictionary/_components/__tests__/DictionaryDetailLayout.test.tsx
ToolLayout.test.tsx / CheatsheetLayout.test.tsx のパターンに倣い、以下のテストケースを実装する:
- パンくずリスト表示: Breadcrumbのnavigation[aria-label="パンくずリスト"]が表示される
- children表示: childrenが正しくレンダリングされる
- TrustLevelBadge表示: meta.trustLevelに基づいたバッジが表示される
- FaqSection表示(faqあり): meta.faqが存在する場合、「よくある質問」セクションが表示される
- FaqSection非表示(faqなし): meta.faqが未設定の場合、FAQセクションは表示されない
- valueProposition表示: meta.valuePropositionが存在する場合、そのテキストが表示される
- valueProposition非表示: meta.valuePropositionが未設定の場合、表示されない
- ShareButtons表示: シェアボタンが表示される
- JSON-LD出力(単一オブジェクト): scriptタグが1つ出力される
- JSON-LD出力(配列): scriptタグが配列の要素数分出力される
テスト用のモックメタデータ:
const mockMeta: DictionaryMeta = {
slug: "test-dict",
name: "テスト辞典",
trustLevel: "curated",
valueProposition: "テスト用の一行価値テキスト",
faq: [
{ question: "テスト質問1?", answer: "テスト回答1。" },
{ question: "テスト質問2?", answer: "テスト回答2。" },
],
};
変更対象ファイルの一覧
新規作成
| ファイル | 内容 |
|---|---|
src/dictionary/_lib/dictionary-meta.ts |
3辞典のDictionaryMeta定数 |
src/dictionary/_components/DictionaryDetailLayout.tsx |
共通Layoutコンポーネント |
src/dictionary/_components/DictionaryDetailLayout.module.css |
Layout用CSS |
src/dictionary/_components/__tests__/DictionaryDetailLayout.test.tsx |
Layoutテスト |
変更
| ファイル | 変更内容 |
|---|---|
src/dictionary/_lib/types.ts |
DictionaryMetaインターフェースの追加 |
src/app/dictionary/kanji/[char]/page.tsx |
DictionaryDetailLayout使用に移行 |
src/app/dictionary/yoji/[yoji]/page.tsx |
DictionaryDetailLayout使用に移行 |
src/app/colors/[slug]/page.tsx |
DictionaryDetailLayout使用に移行、重複breadcrumbJsonLd削除 |
削除
| ファイル | 理由 |
|---|---|
src/app/dictionary/kanji/[char]/page.module.css |
shareSectionスタイルがLayout CSSに統合 |
src/app/dictionary/yoji/[yoji]/page.module.css |
同上 |
src/app/colors/[slug]/page.module.css |
同上 |
完了条件
- 3辞典(漢字・四字熟語・伝統色)の詳細ページがDictionaryDetailLayoutを使用している
- 各詳細ページにvaluePropositionテキストが表示される
- 各詳細ページにFAQセクション(各3問)がアコーディオン形式で表示される
- JSON-LDが正しく出力される(伝統色は辞典固有のJSON-LD + Breadcrumbコンポーネントの自動JSON-LD)
- 既存の表示(Breadcrumb, TrustLevelBadge, Detail, ShareButtons)が変わらない
- 3つの重複page.module.cssが削除されている
- DictionaryDetailLayout.test.tsxの全テストがパスする
- 既存テスト(KanjiDetail.test.tsx, YojiDetail.test.tsx)がパスする
npm run buildがエラーなく完了するnpm run lintがエラーなく完了する
注意事項
ColorDetailの"use client"制約: DictionaryDetailLayoutはServer Componentとして実装し、ColorDetailはchildrenとして渡す。DictionaryDetailLayout自体に"use client"は付けないこと。
BreadcrumbコンポーネントのJSON-LD自動出力: Breadcrumbコンポーネントが内部でgenerateBreadcrumbJsonLdを呼び出してJSON-LDを出力している。DictionaryDetailLayoutのjsonLd propsには辞典固有のJSON-LDのみを渡すこと。伝統色ページの手動breadcrumbJsonLd出力は削除する。
URL構造の維持: 伝統色のURL(/colors)は変更しない。B-122で別途検討する。
一覧ページは変更しない: 本タスクのスコープは詳細ページのみ。一覧ページ(kanji/page.tsx, yoji/page.tsx, colors/page.tsx)やカテゴリページは変更しない。
generateMetadata / generateStaticParams: 各ページ固有のため変更不要。
CSSの重複確認: DictionaryDetailLayoutのCSSで.layoutクラスを定義する場合、既存の/dictionary/layout.tsxや/colors/layout.tsxが提供するmaxWidthラッパーと重複しないか確認すること。既存のlayout.tsxが既にmaxWidthを設定している場合、DictionaryDetailLayout側では不要。
snsの配列: 辞典のShareButtonsは
["x", "line", "copy"]を使用(ToolLayoutの["x", "line", "hatena", "copy"]とは異なる)。既存の設定を維持すること。