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

94 lines
2.4 KiB
TypeScript

import { notFound } from "next/navigation";
import { unstable_cache } from "next/cache";
import { prisma } from "@/lib/db";
import { PageReader } from "@/components/PageReader";
import { encodeId } from "@/lib/hashids";
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string; chapter: string }>;
searchParams: Promise<{ resume?: string }>;
};
const getMangaForReader = unstable_cache(
async (slug: string) =>
prisma.manga.findUnique({
where: { slug },
include: {
chapters: {
orderBy: { number: "asc" },
include: { _count: { select: { pages: true } } },
},
},
}),
["reader-manga"],
{ revalidate: 300 }
);
const getChapterPageMeta = unstable_cache(
async (slug: string, chapterNum: number) =>
prisma.page.findMany({
where: { chapter: { number: chapterNum, manga: { slug } } },
orderBy: { number: "asc" },
select: { number: true, width: true, height: true },
}),
["reader-chapter-meta"],
{ revalidate: 3600 }
);
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; chapter: string }>;
}): Promise<Metadata> {
const { slug, chapter } = await params;
const chapterNum = parseInt(chapter, 10);
if (isNaN(chapterNum)) return { title: "Not Found" };
const manga = await getMangaForReader(slug);
if (!manga) return { title: "Not Found" };
return {
title: `${manga.title} — Ch. ${chapterNum}`,
description: `Read chapter ${chapterNum} of ${manga.title}`,
};
}
export default async function ChapterReaderPage({
params,
searchParams,
}: Props) {
const { slug, chapter } = await params;
const { resume } = await searchParams;
const chapterNum = parseInt(chapter, 10);
if (isNaN(chapterNum)) notFound();
const [manga, initialChapterMeta] = await Promise.all([
getMangaForReader(slug),
getChapterPageMeta(slug, chapterNum),
]);
if (!manga) notFound();
const currentChapter = manga.chapters.find((c) => c.number === chapterNum);
if (!currentChapter) notFound();
const allChapters = manga.chapters.map((c) => ({
id: encodeId(c.id),
number: c.number,
title: c.title,
totalPages: c._count.pages,
}));
return (
<PageReader
mangaSlug={manga.slug}
mangaTitle={manga.title}
startChapterNumber={currentChapter.number}
chapters={allChapters}
initialChapterMeta={initialChapterMeta}
resume={resume === "1"}
/>
);
}