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 type { Metadata } from "next";
export const dynamic = "force-dynamic";
export const revalidate = 300;
export const metadata: Metadata = {
title: "Genres",

View File

@ -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();

View File

@ -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 }>;
};

View File

@ -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({

View File

@ -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 }>;
};
export default async function SearchPage({ searchParams }: Props) {
const { q } = await searchParams;
const manga = q
? await prisma.manga.findMany({
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 searchManga(q) : [];
return (
<div className="max-w-7xl mx-auto px-4 py-6">

View File

@ -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<MetadataRoute.Sitemap> {
const manga = await prisma.manga.findMany({