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>
This commit is contained in:
yiekheng 2026-04-16 20:12:51 +08:00
parent 0923dc1dce
commit 90f8f50166
6 changed files with 51 additions and 31 deletions

View File

@ -3,7 +3,7 @@ import { collectGenres } from "@/lib/genres";
import { GenreTabs } from "@/components/GenreTabs"; import { GenreTabs } from "@/components/GenreTabs";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const dynamic = "force-dynamic"; export const revalidate = 300;
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Genres", title: "Genres",

View File

@ -1,4 +1,5 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { unstable_cache } from "next/cache";
import { prisma } from "@/lib/db"; import { prisma } from "@/lib/db";
import { PageReader } from "@/components/PageReader"; import { PageReader } from "@/components/PageReader";
import { encodeId } from "@/lib/hashids"; import { encodeId } from "@/lib/hashids";
@ -9,6 +10,32 @@ type Props = {
searchParams: Promise<{ resume?: 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({ export async function generateMetadata({
params, params,
}: { }: {
@ -18,7 +45,7 @@ export async function generateMetadata({
const chapterNum = parseInt(chapter, 10); const chapterNum = parseInt(chapter, 10);
if (isNaN(chapterNum)) return { title: "Not Found" }; 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" }; if (!manga) return { title: "Not Found" };
return { return {
@ -37,22 +64,8 @@ export default async function ChapterReaderPage({
if (isNaN(chapterNum)) notFound(); if (isNaN(chapterNum)) notFound();
const [manga, initialChapterMeta] = await Promise.all([ const [manga, initialChapterMeta] = await Promise.all([
prisma.manga.findUnique({ getMangaForReader(slug),
where: { slug }, getChapterPageMeta(slug, chapterNum),
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 },
}),
]); ]);
if (!manga) notFound(); if (!manga) notFound();

View File

@ -5,6 +5,8 @@ import { ChapterList } from "@/components/ChapterList";
import { ReadingProgressButton } from "@/components/ReadingProgressButton"; import { ReadingProgressButton } from "@/components/ReadingProgressButton";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const revalidate = 300;
type Props = { type Props = {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}; };

View File

@ -3,7 +3,7 @@ import { collectGenres } from "@/lib/genres";
import { TrendingCarousel } from "@/components/TrendingCarousel"; import { TrendingCarousel } from "@/components/TrendingCarousel";
import { GenreTabs } from "@/components/GenreTabs"; import { GenreTabs } from "@/components/GenreTabs";
export const dynamic = "force-dynamic"; export const revalidate = 300;
export default async function Home() { export default async function Home() {
const manga = await prisma.manga.findMany({ const manga = await prisma.manga.findMany({

View File

@ -1,3 +1,4 @@
import { unstable_cache } from "next/cache";
import { prisma } from "@/lib/db"; import { prisma } from "@/lib/db";
import { MangaGrid } from "@/components/MangaGrid"; import { MangaGrid } from "@/components/MangaGrid";
import type { Metadata } from "next"; import type { Metadata } from "next";
@ -10,19 +11,23 @@ type Props = {
searchParams: Promise<{ q?: string }>; 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) { export default async function SearchPage({ searchParams }: Props) {
const { q } = await searchParams; const { q } = await searchParams;
const manga = q ? await searchManga(q) : [];
const manga = q
? await prisma.manga.findMany({
where: {
status: "PUBLISHED",
title: { contains: q, mode: "insensitive" },
},
orderBy: { title: "asc" },
include: { _count: { select: { chapters: true } } },
})
: [];
return ( return (
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">

View File

@ -1,7 +1,7 @@
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { prisma } from "@/lib/db"; import { prisma } from "@/lib/db";
export const dynamic = "force-dynamic"; export const revalidate = 3600;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const manga = await prisma.manga.findMany({ const manga = await prisma.manga.findMany({