Skip to content

Commit 3f4cf11

Browse files
committed
perf: implement ISR caching with tag-based revalidation
- Create sanity/lib/fetch.ts: ISR-compatible sanityFetch with cache tags - Production: uses Sanity CDN + Next.js revalidate + cache tags - Draft mode: bypasses cache, uses token for draft content - Update sanity/lib/live.ts: re-export new sanityFetch, keep SanityLive for drafts - Fix sanity/lib/token.ts: warn instead of throw when token missing - Create /api/webhooks/sanity-revalidate: on-demand cache invalidation - Add generateStaticParams to 13 dynamic routes (posts, podcasts, courses, etc.) - Add revalidate exports to 20 pages (60s listings, 3600s detail, 86400s static) - Add cache tags to all sanityFetch calls across 24 files - Conditionally render SanityLive + VisualEditing only in draft mode Expected impact: - TTFB: 300-800ms → 50-100ms (edge cached) - Sanity API calls: every page view → 1 per revalidation period - Content freshness: 60s for listings, on-demand for detail pages
1 parent f39e91f commit 3f4cf11

File tree

28 files changed

+346
-20
lines changed

28 files changed

+346
-20
lines changed

app/(main)/(author)/author/[slug]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import UserRelated from "@/components/user-related";
1919

2020
type Params = Promise<{ slug: string }>;
2121

22+
export const revalidate = 3600;
23+
2224
export async function generateMetadata(
2325
{ params }: { params: Params },
2426
parent: ResolvingMetadata,
@@ -30,6 +32,7 @@ export async function generateMetadata(
3032
query: authorQuery,
3133
params: { slug },
3234
stega: false,
35+
tags: ["author", `author:${slug}`],
3336
})
3437
).data as AuthorQueryResult;
3538

@@ -45,13 +48,23 @@ export async function generateMetadata(
4548
} satisfies Metadata;
4649
}
4750

51+
export async function generateStaticParams() {
52+
const { data } = await sanityFetch({
53+
query: groq`*[_type == "author" && defined(slug.current)].slug.current`,
54+
tags: ["author-list"],
55+
stega: false,
56+
});
57+
return (data as string[]).map((slug) => ({ slug }));
58+
}
59+
4860
export default async function AuthorPage({ params }: { params: Params }) {
4961
const { slug } = await params;
5062

5163
const [authorFetch] = await Promise.all([
5264
sanityFetch({
5365
query: authorQueryWithRelated,
5466
params: { slug },
67+
tags: ["author", `author:${slug}`],
5568
}),
5669
]);
5770

app/(main)/(author)/authors/page/[num]/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live";
44

55
import PaginateList from "@/components/paginate-list";
66
import { docCount } from "@/sanity/lib/queries";
7+
import { groq } from "next-sanity";
78

89
const LIMIT = 10;
910

1011
type Params = Promise<{ num: string }>;
1112

13+
export const revalidate = 60;
14+
15+
export async function generateStaticParams() {
16+
const { data } = await sanityFetch({
17+
query: groq`count(*[_type == "author" && defined(slug.current)])`,
18+
tags: ["author-list"],
19+
stega: false,
20+
});
21+
const count = data as number;
22+
const perPage = LIMIT;
23+
const pages = Math.ceil(count / perPage);
24+
return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) }));
25+
}
26+
1227
export default async function Page({ params }: { params: Params }) {
1328
const { num } = await params;
1429

@@ -18,6 +33,7 @@ export default async function Page({ params }: { params: Params }) {
1833
params: {
1934
type: "author",
2035
},
36+
tags: ["author-list", "author"],
2137
})
2238
).data as DocCountResult;
2339

app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function generateMetadata(
3131
query: lessonQuery,
3232
params: resolvedParams,
3333
stega: false,
34+
tags: ["lesson"],
3435
})
3536
).data as LessonQueryResult;
3637
const previousImages = (await parent).openGraph?.images || [];
@@ -53,8 +54,8 @@ export default async function LessonPage({ params }: { params: Params }) {
5354
const resolvedParams = await params;
5455
const [lesson, course] = (
5556
await Promise.all([
56-
sanityFetch({ query: lessonQuery, params: resolvedParams }),
57-
sanityFetch({ query: lessonsInCourseQuery, params: resolvedParams }),
57+
sanityFetch({ query: lessonQuery, params: resolvedParams, tags: ["lesson"] }),
58+
sanityFetch({ query: lessonsInCourseQuery, params: resolvedParams, tags: ["course", "lesson"] }),
5859
])
5960
).map((res) => res.data) as [
6061
LessonQueryResult,

app/(main)/(course)/course/[courseSlug]/lessons.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default async function Lessons(params: { courseSlug: string }) {
1717
await sanityFetch({
1818
query: lessonsInCourseQuery,
1919
params: { courseSlug },
20+
tags: ["course", "lesson"],
2021
})
2122
).data as LessonsInCourseQueryResult;
2223

app/(main)/(course)/course/[courseSlug]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import ShowPro from "./show-pro";
2121

2222
type Params = Promise<{ courseSlug: string }>;
2323

24+
export const revalidate = 3600;
25+
2426
export async function generateMetadata(
2527
{ params }: { params: Params },
2628
parent: ResolvingMetadata,
@@ -31,6 +33,7 @@ export async function generateMetadata(
3133
query: courseQuery,
3234
params: { courseSlug },
3335
stega: false,
36+
tags: ["course", `course:${courseSlug}`],
3437
})
3538
).data as CourseQueryResult;
3639
const previousImages = (await parent).openGraph?.images || [];
@@ -49,6 +52,15 @@ export async function generateMetadata(
4952
} satisfies Metadata;
5053
}
5154

55+
export async function generateStaticParams() {
56+
const { data } = await sanityFetch({
57+
query: groq`*[_type == "course" && defined(slug.current)].slug.current`,
58+
tags: ["course-list"],
59+
stega: false,
60+
});
61+
return (data as string[]).map((courseSlug) => ({ courseSlug }));
62+
}
63+
5264
export default async function CoursePage({ params }: { params: Params }) {
5365
const { courseSlug } = await params;
5466

@@ -57,6 +69,7 @@ export default async function CoursePage({ params }: { params: Params }) {
5769
query: courseQuery,
5870
params: { courseSlug },
5971
stega: false,
72+
tags: ["course", `course:${courseSlug}`],
6073
})
6174
).data as CourseQueryResult;
6275

app/(main)/(course)/courses/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { sanityFetch } from "@/sanity/lib/live";
1212
import { coursesQuery } from "@/sanity/lib/queries";
1313
import MoreHeader from "@/components/more-header";
1414

15+
export const revalidate = 60;
1516
function HeroCourse({
1617
title,
1718
slug,
@@ -65,7 +66,7 @@ function HeroCourse({
6566

6667
export default async function Page() {
6768
const [heroPost] = (
68-
await Promise.all([sanityFetch({ query: coursesQuery })])
69+
await Promise.all([sanityFetch({ query: coursesQuery, tags: ["course-list", "course"] })])
6970
).map((res) => res.data) as [CoursesQueryResult];
7071

7172
return (

app/(main)/(course)/courses/page/[num]/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live";
44

55
import PaginateList from "@/components/paginate-list";
66
import { docCount } from "@/sanity/lib/queries";
7+
import { groq } from "next-sanity";
78

89
const LIMIT = 10;
910

1011
type Params = Promise<{ num: string }>;
1112

13+
export const revalidate = 60;
14+
15+
export async function generateStaticParams() {
16+
const { data } = await sanityFetch({
17+
query: groq`count(*[_type == "course" && defined(slug.current)])`,
18+
tags: ["course-list"],
19+
stega: false,
20+
});
21+
const count = data as number;
22+
const perPage = LIMIT;
23+
const pages = Math.ceil(count / perPage);
24+
return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) }));
25+
}
26+
1227
export default async function Page({ params }: { params: Params }) {
1328
const [count] = (
1429
await Promise.all([
@@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) {
1732
params: {
1833
type: "course",
1934
},
35+
tags: ["course-list", "course"],
2036
}),
2137
])
2238
).map((res) => res.data) as [DocCountResult];

app/(main)/(guest)/guest/[slug]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import Avatar from "@/components/avatar";
2020

2121
type Params = Promise<{ slug: string }>;
2222

23+
export const revalidate = 3600;
24+
2325
export async function generateMetadata(
2426
{ params }: { params: Params },
2527
parent: ResolvingMetadata,
@@ -31,6 +33,7 @@ export async function generateMetadata(
3133
query: guestQuery,
3234
params: { slug },
3335
stega: false,
36+
tags: ["guest", `guest:${slug}`],
3437
})
3538
).data as GuestQueryResult;
3639
const previousImages = (await parent).openGraph?.images || [];
@@ -45,6 +48,15 @@ export async function generateMetadata(
4548
} satisfies Metadata;
4649
}
4750

51+
export async function generateStaticParams() {
52+
const { data } = await sanityFetch({
53+
query: groq`*[_type == "guest" && defined(slug.current)].slug.current`,
54+
tags: ["guest-list"],
55+
stega: false,
56+
});
57+
return (data as string[]).map((slug) => ({ slug }));
58+
}
59+
4860
export default async function GuestPage({ params }: { params: Params }) {
4961
const { slug } = await params;
5062

@@ -53,6 +65,7 @@ export default async function GuestPage({ params }: { params: Params }) {
5365
sanityFetch({
5466
query: guestQueryWithRelated,
5567
params: { slug },
68+
tags: ["guest", `guest:${slug}`],
5669
}),
5770
])
5871
).map((res) => res.data) as [GuestQueryWithRelatedResult];

app/(main)/(guest)/guests/page/[num]/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live";
44

55
import PaginateList from "@/components/paginate-list";
66
import { docCount } from "@/sanity/lib/queries";
7+
import { groq } from "next-sanity";
78

89
const LIMIT = 10;
910

1011
type Params = Promise<{ num: string }>;
1112

13+
export const revalidate = 60;
14+
15+
export async function generateStaticParams() {
16+
const { data } = await sanityFetch({
17+
query: groq`count(*[_type == "guest" && defined(slug.current)])`,
18+
tags: ["guest-list"],
19+
stega: false,
20+
});
21+
const count = data as number;
22+
const perPage = LIMIT;
23+
const pages = Math.ceil(count / perPage);
24+
return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) }));
25+
}
26+
1227
export default async function Page({ params }: { params: Params }) {
1328
const [count] = (
1429
await Promise.all([
@@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) {
1732
params: {
1833
type: "guest",
1934
},
35+
tags: ["guest-list", "guest"],
2036
}),
2137
])
2238
).map((res) => res.data) as [DocCountResult];

app/(main)/(podcast)/podcast/[slug]/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { sanityFetch } from "@/sanity/lib/live";
55
import { podcastQuery } from "@/sanity/lib/queries";
66
import { resolveOpenGraphImage } from "@/sanity/lib/utils";
77
import Podcast from "../Podcast";
8+
import { groq } from "next-sanity";
89

910
type Params = Promise<{ slug: string }>;
1011

12+
export const revalidate = 3600;
13+
1114
export async function generateMetadata(
1215
{ params }: { params: Params },
1316
parent: ResolvingMetadata,
@@ -19,6 +22,7 @@ export async function generateMetadata(
1922
query: podcastQuery,
2023
params: { slug },
2124
stega: false,
25+
tags: ["podcast", `podcast:${slug}`],
2226
})
2327
).data as PodcastQueryResult;
2428
const previousImages = (await parent).openGraph?.images || [];
@@ -37,6 +41,15 @@ export async function generateMetadata(
3741
} satisfies Metadata;
3842
}
3943

44+
export async function generateStaticParams() {
45+
const { data } = await sanityFetch({
46+
query: groq`*[_type == "podcast" && defined(slug.current)].slug.current`,
47+
tags: ["podcast-list"],
48+
stega: false,
49+
});
50+
return (data as string[]).map((slug) => ({ slug }));
51+
}
52+
4053
export default async function PodcastPage({ params }: { params: Params }) {
4154
const { slug } = await params;
4255

@@ -45,6 +58,7 @@ export default async function PodcastPage({ params }: { params: Params }) {
4558
sanityFetch({
4659
query: podcastQuery,
4760
params: { slug },
61+
tags: ["podcast", `podcast:${slug}`],
4862
}),
4963
])
5064
).map((res) => res.data) as [PodcastQueryResult];

0 commit comments

Comments
 (0)