概要
現在の検索条件(カテゴリ・難易度・キーワード)をパンくずリスト形式で表示し、各ステップをクリックするとその条件だけを外した URL に遷移できる戻り導線を実装する。Server Component で searchParams からパンくず項目を組み立て、<nav aria-label="breadcrumb"> でアクセシブルに実装する。
インストール
# 追加インストールは不要
実装
パンくず項目の組み立て関数
// lib/breadcrumbs.ts
export type BreadcrumbItem = {
label: string;
href: string;
isCurrent: boolean;
};
type FilterParams = {
q?: string;
framework?: string;
category?: string;
difficulty?: string;
};
const LABELS: Record<string, Record<string, string>> = {
framework: { nextjs: "Next.js", react: "React" },
category: {
routing: "ルーティング",
"search-filter": "検索 / フィルタ",
testing: "テスト",
styling: "スタイリング",
},
difficulty: {
beginner: "初級",
intermediate: "中級",
advanced: "上級",
},
};
/** 現在のフィルタ条件からパンくず項目を生成する */
export function buildBreadcrumbs(params: FilterParams): BreadcrumbItem[] {
const items: BreadcrumbItem[] = [
{ label: "サンプル一覧", href: "/samples", isCurrent: false },
];
const accumulated: string[] = [];
for (const [key, value] of Object.entries(params)) {
if (!value) continue;
accumulated.push(`${key}=${encodeURIComponent(value)}`);
const label = LABELS[key]?.[value] ?? value;
items.push({
label,
href: `/samples?${accumulated.join("&")}`,
isCurrent: false,
});
}
if (items.length > 1) {
items[items.length - 1] = {
...items[items.length - 1],
isCurrent: true,
};
}
return items;
}
パンくずコンポーネント
// components/SearchBreadcrumb.tsx
import Link from "next/link";
import type { BreadcrumbItem } from "@/lib/breadcrumbs";
type Props = {
items: BreadcrumbItem[];
};
export function SearchBreadcrumb({ items }: Props) {
if (items.length <= 1) return null;
return (
<nav aria-label="パンくずリスト">
<ol className="flex flex-wrap items-center gap-1 text-sm text-gray-500">
{items.map((item, i) => (
<li key={item.href} className="flex items-center gap-1">
{i > 0 && (
<span aria-hidden="true" className="text-gray-300">
/
</span>
)}
{item.isCurrent ? (
<span aria-current="page" className="font-medium text-gray-700">
{item.label}
</span>
) : (
<Link href={item.href} className="hover:text-blue-600 hover:underline">
{item.label}
</Link>
)}
</li>
))}
</ol>
</nav>
);
}
一覧ページで使う
// app/samples/page.tsx(抜粋)
import { buildBreadcrumbs } from "@/lib/breadcrumbs";
import { SearchBreadcrumb } from "@/components/SearchBreadcrumb";
export default async function SamplesPage({ searchParams }: Props) {
const params = await searchParams;
const q = typeof params.q === "string" ? params.q : undefined;
const framework = typeof params.framework === "string" ? params.framework : undefined;
const category = typeof params.category === "string" ? params.category : undefined;
const difficulty = typeof params.difficulty === "string" ? params.difficulty : undefined;
const breadcrumbs = buildBreadcrumbs({ framework, category, difficulty, q });
return (
<div>
<SearchBreadcrumb items={breadcrumbs} />
{/* ...一覧 */}
</div>
);
}
ポイント
buildBreadcrumbsは pure function として実装する。paramsだけを受け取るのでユニットテストが容易- 条件を1つずつ
accumulatedに積み上げることで、各パンくずステップが「その条件まで」を含む URL になる。クリックすると後続の条件が除去される aria-current="page"を最後の項目(現在地)に付けることで、スクリーンリーダーがどこにいるかを伝えられる<nav aria-label="パンくずリスト">と<ol>を組み合わせることで、支援技術がパンくずナビゲーションとして認識できる- フィルタが何も適用されていない場合(
items.length <= 1)はパンくずを表示しない。「サンプル一覧」だけのパンくずは冗長なため非表示にする LABELSマップで URL パラメータ値を日本語ラベルに変換する。未登録の値はそのまま表示するフォールバックで、taxonomy 追加なしに動作する