yiekheng 90f8f50166 Enable ISR on list pages; cache reader + search DB queries
Stable image URLs (post-signing removal) make page-level caching
safe again. Homepage, genre page, sitemap, and detail page now
revalidate on an interval instead of running Prisma on every hit.
Reader and search keep dynamic rendering (searchParams) but wrap
their Prisma queries in unstable_cache.

TTLs: home/genre/detail 5m, reader manga 5m, reader page meta 1h
(immutable), search 1m, sitemap 1h.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:12:51 +08:00

101 lines
2.9 KiB
TypeScript

import { notFound } from "next/navigation";
import { prisma } from "@/lib/db";
import { parseGenres } from "@/lib/genres";
import { ChapterList } from "@/components/ChapterList";
import { ReadingProgressButton } from "@/components/ReadingProgressButton";
import type { Metadata } from "next";
export const revalidate = 300;
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const manga = await prisma.manga.findUnique({ where: { slug } });
if (!manga) return { title: "Not Found" };
return {
title: manga.title,
description: manga.description,
openGraph: {
title: manga.title,
description: manga.description,
images: [manga.coverUrl],
},
};
}
export default async function MangaDetailPage({ params }: Props) {
const { slug } = await params;
const manga = await prisma.manga.findUnique({
where: { slug },
include: {
chapters: {
orderBy: { number: "asc" },
},
},
});
if (!manga) notFound();
return (
<div className="max-w-3xl mx-auto px-4 py-6">
{/* Hero section */}
<div className="flex gap-4 mb-6">
<div className="w-28 sm:w-36 shrink-0">
<div className="aspect-[3/4] rounded-xl overflow-hidden bg-card">
<img
src={manga.coverUrl}
alt={manga.title}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="flex-1 min-w-0 py-1">
<h1 className="text-xl sm:text-2xl font-bold leading-tight mb-2">
{manga.title}
</h1>
<div className="flex flex-wrap items-center gap-2 mb-3">
{parseGenres(manga.genre).map((g) => (
<span
key={g}
className="px-2 py-0.5 text-[11px] font-semibold bg-accent/20 text-accent rounded-full"
>
{g}
</span>
))}
<span className="px-2 py-0.5 text-[11px] font-semibold bg-surface text-muted rounded-full border border-border">
{manga.status}
</span>
<span className="text-xs text-muted">
{manga.chapters.length} chapter
{manga.chapters.length !== 1 ? "s" : ""}
</span>
</div>
<p className="text-sm text-muted leading-relaxed line-clamp-4 sm:line-clamp-none">
{manga.description}
</p>
</div>
</div>
{manga.chapters.length > 0 && (
<ReadingProgressButton
mangaSlug={manga.slug}
chapters={manga.chapters.map((c) => ({
number: c.number,
title: c.title,
}))}
/>
)}
{/* Chapters */}
<div>
<h2 className="text-lg font-bold mb-3">Chapters</h2>
<ChapterList chapters={manga.chapters} mangaSlug={manga.slug} />
</div>
</div>
);
}