From 90f8f501664bb3a35e004ed635c2398784609d84 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Thu, 16 Apr 2026 20:12:51 +0800 Subject: [PATCH] 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) --- app/genre/page.tsx | 2 +- app/manga/[slug]/[chapter]/page.tsx | 47 ++++++++++++++++++----------- app/manga/[slug]/page.tsx | 2 ++ app/page.tsx | 2 +- app/search/page.tsx | 27 ++++++++++------- app/sitemap.ts | 2 +- 6 files changed, 51 insertions(+), 31 deletions(-) diff --git a/app/genre/page.tsx b/app/genre/page.tsx index f59e3ea..033b691 100644 --- a/app/genre/page.tsx +++ b/app/genre/page.tsx @@ -3,7 +3,7 @@ import { collectGenres } from "@/lib/genres"; import { GenreTabs } from "@/components/GenreTabs"; import type { Metadata } from "next"; -export const dynamic = "force-dynamic"; +export const revalidate = 300; export const metadata: Metadata = { title: "Genres", diff --git a/app/manga/[slug]/[chapter]/page.tsx b/app/manga/[slug]/[chapter]/page.tsx index f8f702f..5d79b38 100644 --- a/app/manga/[slug]/[chapter]/page.tsx +++ b/app/manga/[slug]/[chapter]/page.tsx @@ -1,4 +1,5 @@ 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"; @@ -9,6 +10,32 @@ type Props = { 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, }: { @@ -18,7 +45,7 @@ export async function generateMetadata({ const chapterNum = parseInt(chapter, 10); if (isNaN(chapterNum)) return { title: "Not Found" }; - const manga = await prisma.manga.findUnique({ where: { slug } }); + const manga = await getMangaForReader(slug); if (!manga) return { title: "Not Found" }; return { @@ -37,22 +64,8 @@ export default async function ChapterReaderPage({ if (isNaN(chapterNum)) notFound(); const [manga, initialChapterMeta] = await Promise.all([ - prisma.manga.findUnique({ - where: { slug }, - include: { - chapters: { - orderBy: { number: "asc" }, - include: { - _count: { select: { pages: true } }, - }, - }, - }, - }), - prisma.page.findMany({ - where: { chapter: { number: chapterNum, manga: { slug } } }, - orderBy: { number: "asc" }, - select: { number: true, width: true, height: true }, - }), + getMangaForReader(slug), + getChapterPageMeta(slug, chapterNum), ]); if (!manga) notFound(); diff --git a/app/manga/[slug]/page.tsx b/app/manga/[slug]/page.tsx index c8cbba3..6e17f66 100644 --- a/app/manga/[slug]/page.tsx +++ b/app/manga/[slug]/page.tsx @@ -5,6 +5,8 @@ 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 }>; }; diff --git a/app/page.tsx b/app/page.tsx index ccad489..8579e78 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,7 @@ import { collectGenres } from "@/lib/genres"; import { TrendingCarousel } from "@/components/TrendingCarousel"; import { GenreTabs } from "@/components/GenreTabs"; -export const dynamic = "force-dynamic"; +export const revalidate = 300; export default async function Home() { const manga = await prisma.manga.findMany({ diff --git a/app/search/page.tsx b/app/search/page.tsx index 6e704b6..e4905a6 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -1,3 +1,4 @@ +import { unstable_cache } from "next/cache"; import { prisma } from "@/lib/db"; import { MangaGrid } from "@/components/MangaGrid"; import type { Metadata } from "next"; @@ -10,19 +11,23 @@ type Props = { searchParams: Promise<{ q?: string }>; }; +const searchManga = unstable_cache( + async (q: string) => + prisma.manga.findMany({ + where: { + status: "PUBLISHED", + title: { contains: q, mode: "insensitive" }, + }, + orderBy: { title: "asc" }, + include: { _count: { select: { chapters: true } } }, + }), + ["search-manga"], + { revalidate: 60 } +); + export default async function SearchPage({ searchParams }: Props) { const { q } = await searchParams; - - const manga = q - ? await prisma.manga.findMany({ - where: { - status: "PUBLISHED", - title: { contains: q, mode: "insensitive" }, - }, - orderBy: { title: "asc" }, - include: { _count: { select: { chapters: true } } }, - }) - : []; + const manga = q ? await searchManga(q) : []; return (
diff --git a/app/sitemap.ts b/app/sitemap.ts index 6ff9203..6156d52 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,7 +1,7 @@ import type { MetadataRoute } from "next"; import { prisma } from "@/lib/db"; -export const dynamic = "force-dynamic"; +export const revalidate = 3600; export default async function sitemap(): Promise { const manga = await prisma.manga.findMany({