概要
一覧ページで検索語・フィルタ・sort・page を URL クエリとして保持した状態で詳細ページへ遷移し、詳細ページの「一覧へ戻る」リンクで同じ条件に戻れるパターンを示す。searchParams をそのまま文字列化して back クエリに渡す方法と、詳細ページ側でデコードして戻りリンクを生成する方法を実装する。
インストール
# 追加インストールは不要
実装
クエリを保持したままカードリンクを生成する
// components/SampleCardLink.tsx
import Link from "next/link";
type Props = {
slug: string;
/** 一覧ページの現在のクエリ文字列(searchParams を URLSearchParams で文字列化したもの) */
listQuery?: string;
children: React.ReactNode;
};
export function SampleCardLink({ slug, listQuery, children }: Props) {
const backParam = listQuery ? `?back=${encodeURIComponent(listQuery)}` : "";
return (
<Link href={`/samples/${slug}${backParam}`}>
{children}
</Link>
);
}
一覧ページで listQuery を組み立てて渡す
// app/samples/page.tsx(抜粋)
import { SampleCardLink } from "@/components/SampleCardLink";
export default async function SamplesPage({ searchParams }: Props) {
const params = await searchParams;
// 現在のクエリ文字列をそのまま文字列化
const listQuery = new URLSearchParams(
Object.entries(params).filter(
(entry): entry is [string, string] => typeof entry[1] === "string"
)
).toString();
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((sample) => (
<SampleCardLink key={sample.slug} slug={sample.slug} listQuery={listQuery}>
<SampleCard sample={sample} query={filter.q} />
</SampleCardLink>
))}
</div>
);
}
詳細ページで「一覧へ戻る」リンクを生成する
// app/samples/[slug]/page.tsx(抜粋)
import Link from "next/link";
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
export default async function SampleDetailPage({ params, searchParams }: Props) {
const { slug } = await params;
const sp = await searchParams;
const back = typeof sp.back === "string" ? sp.back : "";
const backHref = back ? `/samples?${back}` : "/samples";
return (
<div>
<Link
href={backHref}
className="mb-4 inline-flex items-center gap-1 text-sm text-blue-600 hover:underline"
>
← サンプル一覧へ戻る
</Link>
{/* ...詳細コンテンツ */}
</div>
);
}
back クエリを検証してから使う
// lib/navigation.ts
/**
* back クエリを検証して安全な戻り先 URL を返す。
* 外部 URL や不正な文字列を拒否して /samples にフォールバックする。
*/
export function safeBackHref(back: string | undefined): string {
if (!back) return "/samples";
try {
// URLSearchParams として解釈できるかチェック
const decoded = decodeURIComponent(back);
new URLSearchParams(decoded); // 不正な場合は例外
return `/samples?${decoded}`;
} catch {
return "/samples";
}
}
// app/samples/[slug]/page.tsx(安全版)
import { safeBackHref } from "@/lib/navigation";
const backHref = safeBackHref(typeof sp.back === "string" ? sp.back : undefined);
ポイント
backパラメータには一覧ページのクエリ文字列をそのままencodeURIComponentして渡す。デコード後に/samples?${back}として使えるシンプルな設計backクエリの値は必ず検証してから使う。外部サイトへの open redirect を防ぐため、/samples?プレフィックスを付けて内部 URL にのみ使用するnew URLSearchParams(decoded)で解析できるかチェックすることで、不正な文字列を安全に弾けるbackがない場合(直接アクセスや別経路)は/samplesにフォールバックする。backを必須にすると直アクセス時にリンクが消えるpageを含むクエリを保持するので、2 ページ目を見ていたユーザーが詳細を見て戻ったとき、同じページ位置に戻ることができる