From 3f4cf1181aad00554a3553df087fcf8631d51763 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 21:51:29 -0500 Subject: [PATCH 01/19] perf: implement ISR caching with tag-based revalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/(main)/(author)/author/[slug]/page.tsx | 13 +++++ .../(author)/authors/page/[num]/page.tsx | 16 ++++++ .../[courseSlug]/lesson/[lessonSlug]/page.tsx | 5 +- .../(course)/course/[courseSlug]/lessons.tsx | 1 + .../(course)/course/[courseSlug]/page.tsx | 13 +++++ app/(main)/(course)/courses/page.tsx | 3 +- .../(course)/courses/page/[num]/page.tsx | 16 ++++++ app/(main)/(guest)/guest/[slug]/page.tsx | 13 +++++ app/(main)/(guest)/guests/page/[num]/page.tsx | 16 ++++++ app/(main)/(podcast)/podcast/[slug]/page.tsx | 14 +++++ app/(main)/(podcast)/podcasts/page.tsx | 2 + .../(podcast)/podcasts/page/[num]/page.tsx | 16 ++++++ app/(main)/(post)/blog/page.tsx | 3 +- app/(main)/(post)/blog/page/[num]/page.tsx | 16 ++++++ app/(main)/(post)/post/[slug]/page.tsx | 13 +++++ app/(main)/(sponsor)/sponsor/[slug]/page.tsx | 13 +++++ .../(sponsor)/sponsors/page/[num]/page.tsx | 16 ++++++ app/(main)/(top-level-pages)/[slug]/page.tsx | 15 ++++- app/(main)/(top-level-pages)/pro/page.tsx | 4 ++ .../(top-level-pages)/sponsorships/page.tsx | 3 +- app/(main)/layout.tsx | 16 ++++-- app/(main)/page.tsx | 3 + app/api/webhooks/sanity-revalidate/route.ts | 53 +++++++++++++++++ app/sitemap.ts | 3 + lib/rss.ts | 2 + sanity/lib/fetch.ts | 57 +++++++++++++++++++ sanity/lib/live.ts | 17 +++--- sanity/lib/token.ts | 4 +- 28 files changed, 346 insertions(+), 20 deletions(-) create mode 100644 app/api/webhooks/sanity-revalidate/route.ts create mode 100644 sanity/lib/fetch.ts diff --git a/app/(main)/(author)/author/[slug]/page.tsx b/app/(main)/(author)/author/[slug]/page.tsx index 78574899..00170154 100644 --- a/app/(main)/(author)/author/[slug]/page.tsx +++ b/app/(main)/(author)/author/[slug]/page.tsx @@ -19,6 +19,8 @@ import UserRelated from "@/components/user-related"; type Params = Promise<{ slug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -30,6 +32,7 @@ export async function generateMetadata( query: authorQuery, params: { slug }, stega: false, + tags: ["author", `author:${slug}`], }) ).data as AuthorQueryResult; @@ -45,6 +48,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "author" && defined(slug.current)].slug.current`, + tags: ["author-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function AuthorPage({ params }: { params: Params }) { const { slug } = await params; @@ -52,6 +64,7 @@ export default async function AuthorPage({ params }: { params: Params }) { sanityFetch({ query: authorQueryWithRelated, params: { slug }, + tags: ["author", `author:${slug}`], }), ]); diff --git a/app/(main)/(author)/authors/page/[num]/page.tsx b/app/(main)/(author)/authors/page/[num]/page.tsx index c0f84b99..a747e3d9 100644 --- a/app/(main)/(author)/authors/page/[num]/page.tsx +++ b/app/(main)/(author)/authors/page/[num]/page.tsx @@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "author" && defined(slug.current)])`, + tags: ["author-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const { num } = await params; @@ -18,6 +33,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "author", }, + tags: ["author-list", "author"], }) ).data as DocCountResult; diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx index 91b6dab7..8f610f1e 100644 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx +++ b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx @@ -31,6 +31,7 @@ export async function generateMetadata( query: lessonQuery, params: resolvedParams, stega: false, + tags: ["lesson"], }) ).data as LessonQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -53,8 +54,8 @@ export default async function LessonPage({ params }: { params: Params }) { const resolvedParams = await params; const [lesson, course] = ( await Promise.all([ - sanityFetch({ query: lessonQuery, params: resolvedParams }), - sanityFetch({ query: lessonsInCourseQuery, params: resolvedParams }), + sanityFetch({ query: lessonQuery, params: resolvedParams, tags: ["lesson"] }), + sanityFetch({ query: lessonsInCourseQuery, params: resolvedParams, tags: ["course", "lesson"] }), ]) ).map((res) => res.data) as [ LessonQueryResult, diff --git a/app/(main)/(course)/course/[courseSlug]/lessons.tsx b/app/(main)/(course)/course/[courseSlug]/lessons.tsx index 631a832c..806ee2bc 100644 --- a/app/(main)/(course)/course/[courseSlug]/lessons.tsx +++ b/app/(main)/(course)/course/[courseSlug]/lessons.tsx @@ -17,6 +17,7 @@ export default async function Lessons(params: { courseSlug: string }) { await sanityFetch({ query: lessonsInCourseQuery, params: { courseSlug }, + tags: ["course", "lesson"], }) ).data as LessonsInCourseQueryResult; diff --git a/app/(main)/(course)/course/[courseSlug]/page.tsx b/app/(main)/(course)/course/[courseSlug]/page.tsx index 3f04dc7f..84a5881d 100644 --- a/app/(main)/(course)/course/[courseSlug]/page.tsx +++ b/app/(main)/(course)/course/[courseSlug]/page.tsx @@ -21,6 +21,8 @@ import ShowPro from "./show-pro"; type Params = Promise<{ courseSlug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -31,6 +33,7 @@ export async function generateMetadata( query: courseQuery, params: { courseSlug }, stega: false, + tags: ["course", `course:${courseSlug}`], }) ).data as CourseQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -49,6 +52,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "course" && defined(slug.current)].slug.current`, + tags: ["course-list"], + stega: false, + }); + return (data as string[]).map((courseSlug) => ({ courseSlug })); +} + export default async function CoursePage({ params }: { params: Params }) { const { courseSlug } = await params; @@ -57,6 +69,7 @@ export default async function CoursePage({ params }: { params: Params }) { query: courseQuery, params: { courseSlug }, stega: false, + tags: ["course", `course:${courseSlug}`], }) ).data as CourseQueryResult; diff --git a/app/(main)/(course)/courses/page.tsx b/app/(main)/(course)/courses/page.tsx index fa0bb7b3..42580e14 100644 --- a/app/(main)/(course)/courses/page.tsx +++ b/app/(main)/(course)/courses/page.tsx @@ -12,6 +12,7 @@ import { sanityFetch } from "@/sanity/lib/live"; import { coursesQuery } from "@/sanity/lib/queries"; import MoreHeader from "@/components/more-header"; +export const revalidate = 60; function HeroCourse({ title, slug, @@ -65,7 +66,7 @@ function HeroCourse({ export default async function Page() { const [heroPost] = ( - await Promise.all([sanityFetch({ query: coursesQuery })]) + await Promise.all([sanityFetch({ query: coursesQuery, tags: ["course-list", "course"] })]) ).map((res) => res.data) as [CoursesQueryResult]; return ( diff --git a/app/(main)/(course)/courses/page/[num]/page.tsx b/app/(main)/(course)/courses/page/[num]/page.tsx index 0bb9d0c5..890869b9 100644 --- a/app/(main)/(course)/courses/page/[num]/page.tsx +++ b/app/(main)/(course)/courses/page/[num]/page.tsx @@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "course" && defined(slug.current)])`, + tags: ["course-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const [count] = ( await Promise.all([ @@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "course", }, + tags: ["course-list", "course"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(guest)/guest/[slug]/page.tsx b/app/(main)/(guest)/guest/[slug]/page.tsx index da7fbf04..5c7713ea 100644 --- a/app/(main)/(guest)/guest/[slug]/page.tsx +++ b/app/(main)/(guest)/guest/[slug]/page.tsx @@ -20,6 +20,8 @@ import Avatar from "@/components/avatar"; type Params = Promise<{ slug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -31,6 +33,7 @@ export async function generateMetadata( query: guestQuery, params: { slug }, stega: false, + tags: ["guest", `guest:${slug}`], }) ).data as GuestQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -45,6 +48,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "guest" && defined(slug.current)].slug.current`, + tags: ["guest-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function GuestPage({ params }: { params: Params }) { const { slug } = await params; @@ -53,6 +65,7 @@ export default async function GuestPage({ params }: { params: Params }) { sanityFetch({ query: guestQueryWithRelated, params: { slug }, + tags: ["guest", `guest:${slug}`], }), ]) ).map((res) => res.data) as [GuestQueryWithRelatedResult]; diff --git a/app/(main)/(guest)/guests/page/[num]/page.tsx b/app/(main)/(guest)/guests/page/[num]/page.tsx index 877e3870..e9e8203d 100644 --- a/app/(main)/(guest)/guests/page/[num]/page.tsx +++ b/app/(main)/(guest)/guests/page/[num]/page.tsx @@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "guest" && defined(slug.current)])`, + tags: ["guest-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const [count] = ( await Promise.all([ @@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "guest", }, + tags: ["guest-list", "guest"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(podcast)/podcast/[slug]/page.tsx b/app/(main)/(podcast)/podcast/[slug]/page.tsx index 06c9701c..8f2e6e9f 100644 --- a/app/(main)/(podcast)/podcast/[slug]/page.tsx +++ b/app/(main)/(podcast)/podcast/[slug]/page.tsx @@ -5,9 +5,12 @@ import { sanityFetch } from "@/sanity/lib/live"; import { podcastQuery } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import Podcast from "../Podcast"; +import { groq } from "next-sanity"; type Params = Promise<{ slug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -19,6 +22,7 @@ export async function generateMetadata( query: podcastQuery, params: { slug }, stega: false, + tags: ["podcast", `podcast:${slug}`], }) ).data as PodcastQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -37,6 +41,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "podcast" && defined(slug.current)].slug.current`, + tags: ["podcast-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function PodcastPage({ params }: { params: Params }) { const { slug } = await params; @@ -45,6 +58,7 @@ export default async function PodcastPage({ params }: { params: Params }) { sanityFetch({ query: podcastQuery, params: { slug }, + tags: ["podcast", `podcast:${slug}`], }), ]) ).map((res) => res.data) as [PodcastQueryResult]; diff --git a/app/(main)/(podcast)/podcasts/page.tsx b/app/(main)/(podcast)/podcasts/page.tsx index e7a638a3..83abf9c2 100644 --- a/app/(main)/(podcast)/podcasts/page.tsx +++ b/app/(main)/(podcast)/podcasts/page.tsx @@ -16,6 +16,7 @@ import MoreHeader from "@/components/more-header"; import PodmatchBadge from "@/components/podmatch-badge"; +export const revalidate = 60; function HeroPodcast({ title, slug, @@ -81,6 +82,7 @@ export default async function Page() { await Promise.all([ sanityFetch({ query: podcastsQuery, + tags: ["podcast-list", "podcast"], }), ]) ).map((res) => res.data) as [PodcastsQueryResult]; diff --git a/app/(main)/(podcast)/podcasts/page/[num]/page.tsx b/app/(main)/(podcast)/podcasts/page/[num]/page.tsx index f920156b..d2dced29 100644 --- a/app/(main)/(podcast)/podcasts/page/[num]/page.tsx +++ b/app/(main)/(podcast)/podcasts/page/[num]/page.tsx @@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "podcast" && defined(slug.current)])`, + tags: ["podcast-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const [count] = ( await Promise.all([ @@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "podcast", }, + tags: ["podcast-list", "podcast"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(post)/blog/page.tsx b/app/(main)/(post)/blog/page.tsx index b980d579..56ce4f07 100644 --- a/app/(main)/(post)/blog/page.tsx +++ b/app/(main)/(post)/blog/page.tsx @@ -15,6 +15,7 @@ import { Separator } from "@/components/ui/separator"; import MoreHeader from "@/components/more-header"; +export const revalidate = 60; function HeroPost({ title, slug, @@ -68,7 +69,7 @@ function HeroPost({ export default async function Page() { const [heroPost] = ( - await Promise.all([sanityFetch({ query: blogQuery })]) + await Promise.all([sanityFetch({ query: blogQuery, tags: ["post-list", "post"] })]) ).map((res) => res.data) as [BlogQueryResult]; return (
diff --git a/app/(main)/(post)/blog/page/[num]/page.tsx b/app/(main)/(post)/blog/page/[num]/page.tsx index 1595e67b..f60e79d9 100644 --- a/app/(main)/(post)/blog/page/[num]/page.tsx +++ b/app/(main)/(post)/blog/page/[num]/page.tsx @@ -4,11 +4,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "post" && defined(slug.current)])`, + tags: ["post-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const [count] = ( await Promise.all([ @@ -17,6 +32,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "post", }, + tags: ["post-list", "post"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(post)/post/[slug]/page.tsx b/app/(main)/(post)/post/[slug]/page.tsx index db9deba8..8e85c2cc 100644 --- a/app/(main)/(post)/post/[slug]/page.tsx +++ b/app/(main)/(post)/post/[slug]/page.tsx @@ -19,6 +19,8 @@ import SponsorCard from "@/components/sponsor-card"; type Params = Promise<{ slug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -30,6 +32,7 @@ export async function generateMetadata( query: postQuery, params: { slug }, stega: false, + tags: ["post", `post:${slug}`], }) ).data as PostQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -48,6 +51,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "post" && defined(slug.current)].slug.current`, + tags: ["post-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function PostPage({ params }: { params: Params }) { const { slug } = await params; @@ -56,6 +68,7 @@ export default async function PostPage({ params }: { params: Params }) { sanityFetch({ query: postQuery, params: { slug }, + tags: ["post", `post:${slug}`], }), ]) ).map((res) => res.data) as [PostQueryResult]; diff --git a/app/(main)/(sponsor)/sponsor/[slug]/page.tsx b/app/(main)/(sponsor)/sponsor/[slug]/page.tsx index 711a0df2..e02daa26 100644 --- a/app/(main)/(sponsor)/sponsor/[slug]/page.tsx +++ b/app/(main)/(sponsor)/sponsor/[slug]/page.tsx @@ -19,6 +19,8 @@ import UserRelated from "@/components/user-related"; type Params = Promise<{ slug: string }>; +export const revalidate = 3600; + export async function generateMetadata( { params }: { params: Params }, parent: ResolvingMetadata, @@ -30,6 +32,7 @@ export async function generateMetadata( query: sponsorQuery, params: { slug }, stega: false, + tags: ["sponsor", `sponsor:${slug}`], }) ).data as SponsorQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -44,6 +47,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "sponsor" && defined(slug.current)].slug.current`, + tags: ["sponsor-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function SponsorPage({ params }: { params: Params }) { const { slug } = await params; @@ -52,6 +64,7 @@ export default async function SponsorPage({ params }: { params: Params }) { sanityFetch({ query: sponsorQueryWithRelated, params: { slug }, + tags: ["sponsor", `sponsor:${slug}`], }), ]) ).map((res) => res.data) as [SponsorQueryWithRelatedResult]; diff --git a/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx b/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx index fe418507..74c568e1 100644 --- a/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx +++ b/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx @@ -7,11 +7,26 @@ import { sanityFetch } from "@/sanity/lib/live"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; +import { groq } from "next-sanity"; const LIMIT = 10; type Params = Promise<{ num: string }>; +export const revalidate = 60; + +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`count(*[_type == "sponsor" && defined(slug.current)])`, + tags: ["sponsor-list"], + stega: false, + }); + const count = data as number; + const perPage = LIMIT; + const pages = Math.ceil(count / perPage); + return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); +} + export default async function Page({ params }: { params: Params }) { const [count] = ( await Promise.all([ @@ -20,6 +35,7 @@ export default async function Page({ params }: { params: Params }) { params: { type: "sponsor", }, + tags: ["sponsor-list", "sponsor"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(top-level-pages)/[slug]/page.tsx b/app/(main)/(top-level-pages)/[slug]/page.tsx index 7423e0fd..3c0d0112 100644 --- a/app/(main)/(top-level-pages)/[slug]/page.tsx +++ b/app/(main)/(top-level-pages)/[slug]/page.tsx @@ -1,5 +1,5 @@ import type { Metadata, ResolvingMetadata } from "next"; -import type { PortableTextBlock } from "next-sanity"; +import { groq, type PortableTextBlock } from "next-sanity"; import { notFound } from "next/navigation"; import PortableText from "@/components/portable-text"; @@ -14,6 +14,8 @@ type Props = { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; +export const revalidate = 86400; + export async function generateMetadata( { params, searchParams }: Props, parent: ResolvingMetadata, @@ -25,6 +27,7 @@ export async function generateMetadata( query: pageQuery, params: { slug }, stega: false, + tags: ["page", `page:${slug}`], }) ).data as PageQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -39,6 +42,15 @@ export async function generateMetadata( } satisfies Metadata; } +export async function generateStaticParams() { + const { data } = await sanityFetch({ + query: groq`*[_type == "page" && defined(slug.current)].slug.current`, + tags: ["page-list"], + stega: false, + }); + return (data as string[]).map((slug) => ({ slug })); +} + export default async function PagePage({ params, searchParams }: Props) { const { slug } = await params; @@ -48,6 +60,7 @@ export default async function PagePage({ params, searchParams }: Props) { query: pageQuery, params: { slug }, stega: false, + tags: ["page", `page:${slug}`], }), ]) ).map((res) => res.data) as [PageQueryResult]; diff --git a/app/(main)/(top-level-pages)/pro/page.tsx b/app/(main)/(top-level-pages)/pro/page.tsx index 504ea44c..863daa9d 100644 --- a/app/(main)/(top-level-pages)/pro/page.tsx +++ b/app/(main)/(top-level-pages)/pro/page.tsx @@ -16,6 +16,8 @@ type Props = { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; +export const revalidate = 86400; + export async function generateMetadata( { params, searchParams }: Props, parent: ResolvingMetadata, @@ -27,6 +29,7 @@ export async function generateMetadata( slug: "pro", }, stega: false, + tags: ["page", "pro"], }) ).data as PageQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -49,6 +52,7 @@ export default async function ProPage({ params, searchParams }: Props) { params: { slug: "pro", }, + tags: ["page", "pro"], }), ]) ).map((res) => res.data) as [PageQueryResult]; diff --git a/app/(main)/(top-level-pages)/sponsorships/page.tsx b/app/(main)/(top-level-pages)/sponsorships/page.tsx index 997dc29b..825a0c86 100644 --- a/app/(main)/(top-level-pages)/sponsorships/page.tsx +++ b/app/(main)/(top-level-pages)/sponsorships/page.tsx @@ -7,6 +7,7 @@ import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SponsorshipForm } from "./sponsorship-form"; +export const revalidate = 86400; const sponsorshipTiers = [ { name: "Dedicated Video", @@ -53,7 +54,7 @@ export async function generateMetadata( await sanityFetch({ query: pageQuery, params: { slug: "sponsorships" }, - tags: ["page:sponsorships"], + tags: ["page", "sponsorships"], }) ).data as PageQueryResult; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 850c14a4..d988c8e6 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -33,6 +33,7 @@ import { PlayerProvider } from "@/components/player-context"; import { toPlainText } from "next-sanity"; import { VisualEditing } from "next-sanity/visual-editing"; import { DisableDraftMode } from "@/components/disable-draft-mode"; +import { draftMode } from "next/headers"; import { ModeToggle } from "@/components/mode-toggle"; import { SiteAnalytics } from "@/components/analytics"; @@ -52,6 +53,7 @@ export async function generateMetadata(): Promise { query: settingsQuery, // Metadata should never contain stega stega: false, + tags: ["settings"], }); const settings = settingsFetch.data as SettingsQueryResult; @@ -87,8 +89,10 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { + const { isEnabled: isDraftMode } = await draftMode(); const settingsFetch = await sanityFetch({ query: settingsQuery, + tags: ["settings"], }); const settings = settingsFetch.data as SettingsQueryResult; @@ -102,10 +106,12 @@ export default async function RootLayout({ )} > - - - - + {isDraftMode && ( + + + + + )} - + {isDraftMode && } diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 9338e4e8..9ebbc33f 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -8,10 +8,13 @@ import { homePageQuery } from "@/sanity/lib/queries"; import Link from "next/link"; import CoverMedia from "@/components/cover-media"; +export const revalidate = 60; + export default async function HomePage() { const [homePageFetch] = await Promise.all([ sanityFetch({ query: homePageQuery, + tags: ["home", "post", "podcast"], }), ]); diff --git a/app/api/webhooks/sanity-revalidate/route.ts b/app/api/webhooks/sanity-revalidate/route.ts new file mode 100644 index 00000000..37092581 --- /dev/null +++ b/app/api/webhooks/sanity-revalidate/route.ts @@ -0,0 +1,53 @@ +import { revalidateTag } from "next/cache"; +import { type NextRequest, NextResponse } from "next/server"; +import { parseBody } from "next-sanity/webhook"; + +export async function POST(request: NextRequest) { + try { + const { isValidSignature, body } = await parseBody<{ + _type: string; + slug?: string; + }>(request, process.env.SANITY_REVALIDATE_SECRET); + + if (!isValidSignature) { + return NextResponse.json( + { message: "Invalid signature" }, + { status: 401 }, + ); + } + + if (!body?._type) { + return NextResponse.json( + { message: "Bad request" }, + { status: 400 }, + ); + } + + // Revalidate by content type + revalidateTag(body._type); + + // Revalidate specific slug + if (body.slug) { + revalidateTag(`${body._type}:${body.slug}`); + } + + // Always revalidate home page and settings when any content changes + revalidateTag("home"); + + // Revalidate listing pages + revalidateTag(`${body._type}-list`); + + return NextResponse.json({ + revalidated: true, + type: body._type, + slug: body.slug, + now: Date.now(), + }); + } catch (error) { + console.error("Revalidation error:", error); + return NextResponse.json( + { message: "Error revalidating" }, + { status: 500 }, + ); + } +} diff --git a/app/sitemap.ts b/app/sitemap.ts index 7b691740..59ae6d08 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -4,6 +4,8 @@ import { sanityFetch } from "@/sanity/lib/live"; import type { SitemapQueryResult } from "@/sanity/types"; import { ContentType } from "@/lib/types"; +export const revalidate = 3600; + export default async function sitemap(): Promise { const productionDomain = process.env.VERCEL_PROJECT_PRODUCTION_URL; @@ -14,6 +16,7 @@ export default async function sitemap(): Promise { const content = ( await sanityFetch({ query: sitemapQuery, + tags: ["sitemap"], }) ).data as SitemapQueryResult; diff --git a/lib/rss.ts b/lib/rss.ts index 8797c03c..7f167ebe 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -42,6 +42,7 @@ export async function buildFeed(params: { limit: params.limit || 10000, offset: params.offset || 0, }, + tags: isPodcast ? ["podcast-rss", "podcast"] : [params.type + "-rss", params.type], }) ).data as RssQueryResult; @@ -140,6 +141,7 @@ export async function buildPodcastFeed(params: { limit: params.limit || 10000, offset: params.offset || 0, }, + tags: ["podcast-rss", "podcast"], }) ).data as RssQueryResult; diff --git a/sanity/lib/fetch.ts b/sanity/lib/fetch.ts new file mode 100644 index 00000000..f3e27587 --- /dev/null +++ b/sanity/lib/fetch.ts @@ -0,0 +1,57 @@ +import "server-only"; + +import type { QueryParams } from "next-sanity"; +import { draftMode } from "next/headers"; +import { client } from "@/sanity/lib/client"; +import { token } from "@/sanity/lib/token"; + +/** + * Server-only Sanity fetch with ISR caching and tag-based revalidation. + * + * In draft mode: bypasses cache, uses token for draft content. + * In production: uses CDN with Next.js cache tags for on-demand revalidation. + */ +export async function sanityFetch({ + query, + params = {}, + tags = [], + revalidate, + stega, +}: { + query: string; + params?: QueryParams; + tags?: string[]; + revalidate?: number | false; + stega?: boolean; +}): Promise<{ data: T }> { + const isDraft = (await draftMode()).isEnabled; + + if (isDraft) { + // Draft mode: bypass cache, use token, get draft content + const data = await client + .withConfig({ + useCdn: false, + token: token || undefined, + perspective: "drafts", + stega: stega !== undefined ? { enabled: stega } : undefined, + }) + .fetch(query, params, { + cache: "no-store", + }); + return { data }; + } + + // Production: use CDN with ISR caching + const data = await client + .withConfig({ + useCdn: true, + stega: stega !== undefined ? { enabled: stega } : undefined, + }) + .fetch(query, params, { + next: { + revalidate: revalidate ?? 60, + tags, + }, + }); + return { data }; +} diff --git a/sanity/lib/live.ts b/sanity/lib/live.ts index 413dad02..4f835bd5 100644 --- a/sanity/lib/live.ts +++ b/sanity/lib/live.ts @@ -1,14 +1,15 @@ -// Querying with "sanityFetch" will keep content automatically updated -// Before using it, import and render "" in your layout, see -// https://github.com/sanity-io/next-sanity#live-content-api for more information. - import { defineLive } from "next-sanity/live"; import { client } from "@/sanity/lib/client"; import { token } from "@/sanity/lib/token"; -import { stegaClean } from "@sanity/client/stega"; -export const { sanityFetch, SanityLive } = defineLive({ +// SanityLive is only used in draft mode for real-time updates +const live = defineLive({ client, - serverToken: token, - browserToken: token, + serverToken: token || undefined, + browserToken: token || undefined, }); + +export const SanityLive = live.SanityLive; + +// Re-export the ISR-compatible sanityFetch +export { sanityFetch } from "@/sanity/lib/fetch"; diff --git a/sanity/lib/token.ts b/sanity/lib/token.ts index 57ec28b5..c04969ed 100644 --- a/sanity/lib/token.ts +++ b/sanity/lib/token.ts @@ -1,5 +1,7 @@ export const token = process.env.SANITY_API_READ_TOKEN; if (!token) { - throw new Error("Missing SANITY_API_READ_TOKEN"); + console.warn( + "Missing SANITY_API_READ_TOKEN — draft mode and live preview will not work", + ); } From a5a884ea7b084812b274dade2923477ed1260d39 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:23:46 -0500 Subject: [PATCH 02/19] perf: align caching with next-sanity defineLive best practices Reverts the custom sanityFetch/fetch.ts approach in favor of defineLive's built-in caching. defineLive automatically: - Sets cache tags (opaque sanity: prefixed tags) - Revalidates via SanityLive component listening to Live Content API - Integrates with ISR so all visitors see fresh content Changes: - Revert sanity/lib/live.ts to standard defineLive pattern - Delete sanity/lib/fetch.ts (not needed) - SanityLive always renders (required for cache revalidation) - Remove manual tags from all sanityFetch calls - Fix generateStaticParams to use client.fetch directly - Keep revalidate exports as safety net (20 pages) - Keep generateStaticParams for build-time pre-rendering (13 routes) - Simplify webhook to revalidateTag("sanity") backup --- app/(main)/(author)/author/[slug]/page.tsx | 13 +++++-------- app/(main)/(author)/authors/page/[num]/page.tsx | 11 ++++------- app/(main)/(guest)/guest/[slug]/page.tsx | 13 +++++-------- app/(main)/(guest)/guests/page/[num]/page.tsx | 11 ++++------- app/(main)/(podcast)/podcast/[slug]/page.tsx | 13 +++++-------- app/(main)/(podcast)/podcasts/page.tsx | 1 - .../(podcast)/podcasts/page/[num]/page.tsx | 11 ++++------- app/(main)/(post)/blog/page.tsx | 2 +- app/(main)/(post)/blog/page/[num]/page.tsx | 11 ++++------- app/(main)/(post)/post/[slug]/page.tsx | 13 +++++-------- app/(main)/(sponsor)/sponsor/[slug]/page.tsx | 13 +++++-------- .../(sponsor)/sponsors/page/[num]/page.tsx | 11 ++++------- app/(main)/(top-level-pages)/[slug]/page.tsx | 13 +++++-------- app/(main)/(top-level-pages)/pro/page.tsx | 2 -- .../(top-level-pages)/sponsorships/page.tsx | 1 - app/(main)/layout.tsx | 16 +++++----------- app/(main)/page.tsx | 1 - app/api/webhooks/sanity-revalidate/route.ts | 16 +++------------- app/sitemap.ts | 1 - lib/rss.ts | 1 - sanity/lib/live.ts | 16 +++++++--------- 21 files changed, 66 insertions(+), 124 deletions(-) diff --git a/app/(main)/(author)/author/[slug]/page.tsx b/app/(main)/(author)/author/[slug]/page.tsx index 00170154..886f9d36 100644 --- a/app/(main)/(author)/author/[slug]/page.tsx +++ b/app/(main)/(author)/author/[slug]/page.tsx @@ -9,6 +9,7 @@ import type { AuthorQueryWithRelatedResult, } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { authorQuery, authorQueryWithRelated } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import CoverMedia from "@/components/cover-media"; @@ -32,7 +33,6 @@ export async function generateMetadata( query: authorQuery, params: { slug }, stega: false, - tags: ["author", `author:${slug}`], }) ).data as AuthorQueryResult; @@ -49,12 +49,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "author" && defined(slug.current)].slug.current`, - tags: ["author-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "author" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function AuthorPage({ params }: { params: Params }) { @@ -64,7 +62,6 @@ export default async function AuthorPage({ params }: { params: Params }) { sanityFetch({ query: authorQueryWithRelated, params: { slug }, - tags: ["author", `author:${slug}`], }), ]); diff --git a/app/(main)/(author)/authors/page/[num]/page.tsx b/app/(main)/(author)/authors/page/[num]/page.tsx index a747e3d9..871b4d0e 100644 --- a/app/(main)/(author)/authors/page/[num]/page.tsx +++ b/app/(main)/(author)/authors/page/[num]/page.tsx @@ -1,6 +1,7 @@ import MoreContent from "@/components/more-content"; import type { DocCountResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; @@ -13,12 +14,9 @@ type Params = Promise<{ num: string }>; export const revalidate = 60; export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "author" && defined(slug.current)])`, - tags: ["author-list"], - stega: false, - }); - const count = data as number; + const count = await client.fetch( + groq`count(*[_type == "author" && defined(slug.current)])`, + ); const perPage = LIMIT; const pages = Math.ceil(count / perPage); return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); @@ -33,7 +31,6 @@ export default async function Page({ params }: { params: Params }) { params: { type: "author", }, - tags: ["author-list", "author"], }) ).data as DocCountResult; diff --git a/app/(main)/(guest)/guest/[slug]/page.tsx b/app/(main)/(guest)/guest/[slug]/page.tsx index 5c7713ea..6a893a89 100644 --- a/app/(main)/(guest)/guest/[slug]/page.tsx +++ b/app/(main)/(guest)/guest/[slug]/page.tsx @@ -9,6 +9,7 @@ import type { GuestQueryWithRelatedResult, } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { guestQuery, guestQueryWithRelated } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import CoverMedia from "@/components/cover-media"; @@ -33,7 +34,6 @@ export async function generateMetadata( query: guestQuery, params: { slug }, stega: false, - tags: ["guest", `guest:${slug}`], }) ).data as GuestQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -49,12 +49,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "guest" && defined(slug.current)].slug.current`, - tags: ["guest-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "guest" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function GuestPage({ params }: { params: Params }) { @@ -65,7 +63,6 @@ export default async function GuestPage({ params }: { params: Params }) { sanityFetch({ query: guestQueryWithRelated, params: { slug }, - tags: ["guest", `guest:${slug}`], }), ]) ).map((res) => res.data) as [GuestQueryWithRelatedResult]; diff --git a/app/(main)/(guest)/guests/page/[num]/page.tsx b/app/(main)/(guest)/guests/page/[num]/page.tsx index e9e8203d..d633f12f 100644 --- a/app/(main)/(guest)/guests/page/[num]/page.tsx +++ b/app/(main)/(guest)/guests/page/[num]/page.tsx @@ -1,6 +1,7 @@ import MoreContent from "@/components/more-content"; import type { DocCountResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; @@ -13,12 +14,9 @@ type Params = Promise<{ num: string }>; export const revalidate = 60; export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "guest" && defined(slug.current)])`, - tags: ["guest-list"], - stega: false, - }); - const count = data as number; + const count = await client.fetch( + groq`count(*[_type == "guest" && defined(slug.current)])`, + ); const perPage = LIMIT; const pages = Math.ceil(count / perPage); return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); @@ -32,7 +30,6 @@ export default async function Page({ params }: { params: Params }) { params: { type: "guest", }, - tags: ["guest-list", "guest"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(podcast)/podcast/[slug]/page.tsx b/app/(main)/(podcast)/podcast/[slug]/page.tsx index 8f2e6e9f..058757e4 100644 --- a/app/(main)/(podcast)/podcast/[slug]/page.tsx +++ b/app/(main)/(podcast)/podcast/[slug]/page.tsx @@ -2,6 +2,7 @@ import type { Metadata, ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; import type { PodcastQueryResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { podcastQuery } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import Podcast from "../Podcast"; @@ -22,7 +23,6 @@ export async function generateMetadata( query: podcastQuery, params: { slug }, stega: false, - tags: ["podcast", `podcast:${slug}`], }) ).data as PodcastQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -42,12 +42,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "podcast" && defined(slug.current)].slug.current`, - tags: ["podcast-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "podcast" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function PodcastPage({ params }: { params: Params }) { @@ -58,7 +56,6 @@ export default async function PodcastPage({ params }: { params: Params }) { sanityFetch({ query: podcastQuery, params: { slug }, - tags: ["podcast", `podcast:${slug}`], }), ]) ).map((res) => res.data) as [PodcastQueryResult]; diff --git a/app/(main)/(podcast)/podcasts/page.tsx b/app/(main)/(podcast)/podcasts/page.tsx index 83abf9c2..fd03f00c 100644 --- a/app/(main)/(podcast)/podcasts/page.tsx +++ b/app/(main)/(podcast)/podcasts/page.tsx @@ -82,7 +82,6 @@ export default async function Page() { await Promise.all([ sanityFetch({ query: podcastsQuery, - tags: ["podcast-list", "podcast"], }), ]) ).map((res) => res.data) as [PodcastsQueryResult]; diff --git a/app/(main)/(podcast)/podcasts/page/[num]/page.tsx b/app/(main)/(podcast)/podcasts/page/[num]/page.tsx index d2dced29..605ab84e 100644 --- a/app/(main)/(podcast)/podcasts/page/[num]/page.tsx +++ b/app/(main)/(podcast)/podcasts/page/[num]/page.tsx @@ -1,6 +1,7 @@ import MoreContent from "@/components/more-content"; import type { DocCountResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; @@ -13,12 +14,9 @@ type Params = Promise<{ num: string }>; export const revalidate = 60; export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "podcast" && defined(slug.current)])`, - tags: ["podcast-list"], - stega: false, - }); - const count = data as number; + const count = await client.fetch( + groq`count(*[_type == "podcast" && defined(slug.current)])`, + ); const perPage = LIMIT; const pages = Math.ceil(count / perPage); return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); @@ -32,7 +30,6 @@ export default async function Page({ params }: { params: Params }) { params: { type: "podcast", }, - tags: ["podcast-list", "podcast"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(post)/blog/page.tsx b/app/(main)/(post)/blog/page.tsx index 56ce4f07..0e284d05 100644 --- a/app/(main)/(post)/blog/page.tsx +++ b/app/(main)/(post)/blog/page.tsx @@ -69,7 +69,7 @@ function HeroPost({ export default async function Page() { const [heroPost] = ( - await Promise.all([sanityFetch({ query: blogQuery, tags: ["post-list", "post"] })]) + await Promise.all([sanityFetch({ query: blogQuery })]) ).map((res) => res.data) as [BlogQueryResult]; return (
diff --git a/app/(main)/(post)/blog/page/[num]/page.tsx b/app/(main)/(post)/blog/page/[num]/page.tsx index f60e79d9..0be78de0 100644 --- a/app/(main)/(post)/blog/page/[num]/page.tsx +++ b/app/(main)/(post)/blog/page/[num]/page.tsx @@ -1,6 +1,7 @@ import MoreContent from "@/components/more-content"; import type { DocCountResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; @@ -13,12 +14,9 @@ type Params = Promise<{ num: string }>; export const revalidate = 60; export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "post" && defined(slug.current)])`, - tags: ["post-list"], - stega: false, - }); - const count = data as number; + const count = await client.fetch( + groq`count(*[_type == "post" && defined(slug.current)])`, + ); const perPage = LIMIT; const pages = Math.ceil(count / perPage); return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); @@ -32,7 +30,6 @@ export default async function Page({ params }: { params: Params }) { params: { type: "post", }, - tags: ["post-list", "post"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(post)/post/[slug]/page.tsx b/app/(main)/(post)/post/[slug]/page.tsx index 8e85c2cc..b735009e 100644 --- a/app/(main)/(post)/post/[slug]/page.tsx +++ b/app/(main)/(post)/post/[slug]/page.tsx @@ -10,6 +10,7 @@ import PortableText from "@/components/portable-text"; import type { PostQueryResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { postQuery } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import CoverMedia from "@/components/cover-media"; @@ -32,7 +33,6 @@ export async function generateMetadata( query: postQuery, params: { slug }, stega: false, - tags: ["post", `post:${slug}`], }) ).data as PostQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -52,12 +52,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "post" && defined(slug.current)].slug.current`, - tags: ["post-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "post" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function PostPage({ params }: { params: Params }) { @@ -68,7 +66,6 @@ export default async function PostPage({ params }: { params: Params }) { sanityFetch({ query: postQuery, params: { slug }, - tags: ["post", `post:${slug}`], }), ]) ).map((res) => res.data) as [PostQueryResult]; diff --git a/app/(main)/(sponsor)/sponsor/[slug]/page.tsx b/app/(main)/(sponsor)/sponsor/[slug]/page.tsx index e02daa26..383792aa 100644 --- a/app/(main)/(sponsor)/sponsor/[slug]/page.tsx +++ b/app/(main)/(sponsor)/sponsor/[slug]/page.tsx @@ -9,6 +9,7 @@ import type { SponsorQueryWithRelatedResult, } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { sponsorQuery, sponsorQueryWithRelated } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import CoverMedia from "@/components/cover-media"; @@ -32,7 +33,6 @@ export async function generateMetadata( query: sponsorQuery, params: { slug }, stega: false, - tags: ["sponsor", `sponsor:${slug}`], }) ).data as SponsorQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -48,12 +48,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "sponsor" && defined(slug.current)].slug.current`, - tags: ["sponsor-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "sponsor" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function SponsorPage({ params }: { params: Params }) { @@ -64,7 +62,6 @@ export default async function SponsorPage({ params }: { params: Params }) { sanityFetch({ query: sponsorQueryWithRelated, params: { slug }, - tags: ["sponsor", `sponsor:${slug}`], }), ]) ).map((res) => res.data) as [SponsorQueryWithRelatedResult]; diff --git a/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx b/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx index 74c568e1..af3d45c1 100644 --- a/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx +++ b/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import MoreContent from "@/components/more-content"; import type { DocCountResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import PaginateList from "@/components/paginate-list"; import { docCount } from "@/sanity/lib/queries"; @@ -16,12 +17,9 @@ type Params = Promise<{ num: string }>; export const revalidate = 60; export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "sponsor" && defined(slug.current)])`, - tags: ["sponsor-list"], - stega: false, - }); - const count = data as number; + const count = await client.fetch( + groq`count(*[_type == "sponsor" && defined(slug.current)])`, + ); const perPage = LIMIT; const pages = Math.ceil(count / perPage); return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); @@ -35,7 +33,6 @@ export default async function Page({ params }: { params: Params }) { params: { type: "sponsor", }, - tags: ["sponsor-list", "sponsor"], }), ]) ).map((res) => res.data) as [DocCountResult]; diff --git a/app/(main)/(top-level-pages)/[slug]/page.tsx b/app/(main)/(top-level-pages)/[slug]/page.tsx index 3c0d0112..e325e877 100644 --- a/app/(main)/(top-level-pages)/[slug]/page.tsx +++ b/app/(main)/(top-level-pages)/[slug]/page.tsx @@ -6,6 +6,7 @@ import PortableText from "@/components/portable-text"; import type { PageQueryResult } from "@/sanity/types"; import { sanityFetch } from "@/sanity/lib/live"; +import { client } from "@/sanity/lib/client"; import { pageQuery } from "@/sanity/lib/queries"; import { resolveOpenGraphImage } from "@/sanity/lib/utils"; @@ -27,7 +28,6 @@ export async function generateMetadata( query: pageQuery, params: { slug }, stega: false, - tags: ["page", `page:${slug}`], }) ).data as PageQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -43,12 +43,10 @@ export async function generateMetadata( } export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "page" && defined(slug.current)].slug.current`, - tags: ["page-list"], - stega: false, - }); - return (data as string[]).map((slug) => ({ slug })); + const slugs = await client.fetch( + groq`*[_type == "page" && defined(slug.current)].slug.current`, + ); + return slugs.map((slug) => ({ slug })); } export default async function PagePage({ params, searchParams }: Props) { @@ -60,7 +58,6 @@ export default async function PagePage({ params, searchParams }: Props) { query: pageQuery, params: { slug }, stega: false, - tags: ["page", `page:${slug}`], }), ]) ).map((res) => res.data) as [PageQueryResult]; diff --git a/app/(main)/(top-level-pages)/pro/page.tsx b/app/(main)/(top-level-pages)/pro/page.tsx index 863daa9d..6c67c04e 100644 --- a/app/(main)/(top-level-pages)/pro/page.tsx +++ b/app/(main)/(top-level-pages)/pro/page.tsx @@ -29,7 +29,6 @@ export async function generateMetadata( slug: "pro", }, stega: false, - tags: ["page", "pro"], }) ).data as PageQueryResult; const previousImages = (await parent).openGraph?.images || []; @@ -52,7 +51,6 @@ export default async function ProPage({ params, searchParams }: Props) { params: { slug: "pro", }, - tags: ["page", "pro"], }), ]) ).map((res) => res.data) as [PageQueryResult]; diff --git a/app/(main)/(top-level-pages)/sponsorships/page.tsx b/app/(main)/(top-level-pages)/sponsorships/page.tsx index 825a0c86..d0aadf63 100644 --- a/app/(main)/(top-level-pages)/sponsorships/page.tsx +++ b/app/(main)/(top-level-pages)/sponsorships/page.tsx @@ -54,7 +54,6 @@ export async function generateMetadata( await sanityFetch({ query: pageQuery, params: { slug: "sponsorships" }, - tags: ["page", "sponsorships"], }) ).data as PageQueryResult; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index d988c8e6..850c14a4 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -33,7 +33,6 @@ import { PlayerProvider } from "@/components/player-context"; import { toPlainText } from "next-sanity"; import { VisualEditing } from "next-sanity/visual-editing"; import { DisableDraftMode } from "@/components/disable-draft-mode"; -import { draftMode } from "next/headers"; import { ModeToggle } from "@/components/mode-toggle"; import { SiteAnalytics } from "@/components/analytics"; @@ -53,7 +52,6 @@ export async function generateMetadata(): Promise { query: settingsQuery, // Metadata should never contain stega stega: false, - tags: ["settings"], }); const settings = settingsFetch.data as SettingsQueryResult; @@ -89,10 +87,8 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - const { isEnabled: isDraftMode } = await draftMode(); const settingsFetch = await sanityFetch({ query: settingsQuery, - tags: ["settings"], }); const settings = settingsFetch.data as SettingsQueryResult; @@ -106,12 +102,10 @@ export default async function RootLayout({ )} > - {isDraftMode && ( - - - - - )} + + + + - {isDraftMode && } + diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 9ebbc33f..21da33b4 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -14,7 +14,6 @@ export default async function HomePage() { const [homePageFetch] = await Promise.all([ sanityFetch({ query: homePageQuery, - tags: ["home", "post", "podcast"], }), ]); diff --git a/app/api/webhooks/sanity-revalidate/route.ts b/app/api/webhooks/sanity-revalidate/route.ts index 37092581..cf39938a 100644 --- a/app/api/webhooks/sanity-revalidate/route.ts +++ b/app/api/webhooks/sanity-revalidate/route.ts @@ -23,19 +23,9 @@ export async function POST(request: NextRequest) { ); } - // Revalidate by content type - revalidateTag(body._type); - - // Revalidate specific slug - if (body.slug) { - revalidateTag(`${body._type}:${body.slug}`); - } - - // Always revalidate home page and settings when any content changes - revalidateTag("home"); - - // Revalidate listing pages - revalidateTag(`${body._type}-list`); + // Revalidate all sanity-tagged caches (the "heavy hammer" approach) + // This is a backup for when no visitors are active to trigger SanityLive revalidation + revalidateTag("sanity"); return NextResponse.json({ revalidated: true, diff --git a/app/sitemap.ts b/app/sitemap.ts index 59ae6d08..c61ac2f4 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -16,7 +16,6 @@ export default async function sitemap(): Promise { const content = ( await sanityFetch({ query: sitemapQuery, - tags: ["sitemap"], }) ).data as SitemapQueryResult; diff --git a/lib/rss.ts b/lib/rss.ts index 7f167ebe..283b962a 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -141,7 +141,6 @@ export async function buildPodcastFeed(params: { limit: params.limit || 10000, offset: params.offset || 0, }, - tags: ["podcast-rss", "podcast"], }) ).data as RssQueryResult; diff --git a/sanity/lib/live.ts b/sanity/lib/live.ts index 4f835bd5..ee013787 100644 --- a/sanity/lib/live.ts +++ b/sanity/lib/live.ts @@ -1,15 +1,13 @@ +// Querying with "sanityFetch" will keep content automatically updated +// Before using it, import and render "" in your layout, see +// https://github.com/sanity-io/next-sanity#live-content-api for more information. + import { defineLive } from "next-sanity/live"; import { client } from "@/sanity/lib/client"; import { token } from "@/sanity/lib/token"; -// SanityLive is only used in draft mode for real-time updates -const live = defineLive({ +export const { sanityFetch, SanityLive } = defineLive({ client, - serverToken: token || undefined, - browserToken: token || undefined, + serverToken: token, + browserToken: token, }); - -export const SanityLive = live.SanityLive; - -// Re-export the ISR-compatible sanityFetch -export { sanityFetch } from "@/sanity/lib/fetch"; From f132e8f373dcb9215d953284785b96c3880f5863 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:05 -0500 Subject: [PATCH 03/19] chore: remove course route file lesson-client-only.tsx --- .../[lessonSlug]/lesson-client-only.tsx | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-client-only.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-client-only.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-client-only.tsx deleted file mode 100644 index 59c1c138..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-client-only.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; -import type { - LessonQueryResult, - LessonsInCourseQueryResult, -} from "@/sanity/types"; -import { useEffect, useState } from "react"; -import LessonPanel from "./lesson-panel"; - -export default function LessonPanelClientOnly({ - lesson, - course, -}: { - lesson: NonNullable; - course: NonNullable; -}) { - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - }, []); - - //TODO: Make this match better? - if (!isClient) return
Loading Lesson...
; - - return ( - <> - - - ); -} From f9dc15c567d2c9e98f14e7c0965d26ff8848a6c1 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:06 -0500 Subject: [PATCH 04/19] chore: remove course route file lesson-complete.tsx --- .../[courseSlug]/lesson/[lessonSlug]/lesson-complete.tsx | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-complete.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-complete.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-complete.tsx deleted file mode 100644 index 05de57ef..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-complete.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; - -export default function LessonComplete() { - return <>; -} From b6bb02af9e62f44ab13c5e0efc235f7510048c6f Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:07 -0500 Subject: [PATCH 05/19] chore: remove course route file lesson-panel.tsx --- .../lesson/[lessonSlug]/lesson-panel.tsx | 153 ------------------ 1 file changed, 153 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-panel.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-panel.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-panel.tsx deleted file mode 100644 index aa05e29d..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-panel.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable"; -import Link from "next/link"; - -import type { - LessonQueryResult, - LessonsInCourseQueryResult, -} from "@/sanity/types"; -import BadgePro from "@/components/badge-pro"; -import NavLesson from "./nav-lesson"; -import CoverMedia from "@/components/cover-media"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Button } from "@/components/ui/button"; -import { - FaCircleArrowLeft, - FaCircleArrowRight, - FaHouse, -} from "react-icons/fa6"; - -import { useLocalStorage } from "@uidotdev/usehooks"; - -export default function LessonPanel({ - lesson, - course, -}: { - lesson: NonNullable; - course: NonNullable; -}) { - const [defaultLayout, saveDefaultLayout] = useLocalStorage( - "codingcatdev:lesson:layout", - [25, 75], - ); - - const onLayout = (sizes: number[]) => { - saveDefaultLayout(sizes); - }; - - const getLessons = () => { - const lessons: NonNullable< - NonNullable["sections"] - >[0]["lesson"] = []; - course?.sections?.map((section) => - section.lesson?.map((lesson) => lessons.push(lesson)), - ); - return lessons; - }; - - const lessonIndex = getLessons().findIndex((l) => l.slug === lesson.slug); - const lessonNoContent = getLessons()[lessonIndex]; - - const main = () => { - return ( -
-
-
-

{lesson?.title}

-
- -
-
- -
-
-
- {lessonIndex > 0 && ( - - )} - - {lessonIndex < getLessons().length - 1 && ( - - )} -
-
-
- ); - }; - - return ( - <> -
- - - {course?.sections && ( - <> -
- - {course.title} - -
- -
- -
- - )} -
- - - {main()} - -
-
-
- {main()} - - {course?.sections && ( - <> -
- -
- - )} -
-
- - ); -} From 1e0d4f0d8ec90997a364948f043ca9fe993678c4 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:08 -0500 Subject: [PATCH 06/19] chore: remove course route file nav-lesson.tsx --- .../lesson/[lessonSlug]/nav-lesson.tsx | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/nav-lesson.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/nav-lesson.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/nav-lesson.tsx deleted file mode 100644 index 98107a89..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/nav-lesson.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import type { LessonsInCourseQueryResult } from "@/sanity/types"; -import Link from "next/link"; - -import { useActivePath } from "@/lib/hooks"; -import { Separator } from "@/components/ui/separator"; -import BadgePro from "@/components/badge-pro"; - -interface Props { - course: LessonsInCourseQueryResult | undefined; -} -export default function NavLesson({ course }: Props) { - const checkActivePath = useActivePath(); - return ( - <> - {course?.sections?.map((section, i) => ( -
- {section?.title && ( -
-

- {section.title} -

- -
- )} - {section.lesson?.map((l) => ( -
- - {l.title} - -
- -
-
- ))} -
- ))} - - ); -} From 6f904efbf770374332fc52088c58ba8e480e7c6a Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:09 -0500 Subject: [PATCH 07/19] chore: remove course route file page.tsx --- .../[courseSlug]/lesson/[lessonSlug]/page.tsx | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx b/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx deleted file mode 100644 index 8f610f1e..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -export const dynamic = "force-dynamic"; - -import type { Metadata, ResolvingMetadata } from "next"; -import { notFound, redirect } from "next/navigation"; -import { Suspense } from "react"; - -import type { - LessonQueryResult, - LessonsInCourseQueryResult, -} from "@/sanity/types"; -import { sanityFetch } from "@/sanity/lib/live"; -import { lessonQuery, lessonsInCourseQuery } from "@/sanity/lib/queries"; -import { resolveOpenGraphImage } from "@/sanity/lib/utils"; -import LessonPanelClientOnly from "./lesson-client-only"; -import MoreContent from "@/components/more-content"; -import MoreHeader from "@/components/more-header"; -import PortableText from "@/components/portable-text"; -import type { PortableTextBlock } from "next-sanity"; -import { cookies } from "next/headers"; -import { jwtDecode } from "jwt-decode"; - -type Params = Promise<{ lessonSlug: string; courseSlug: string }>; - -export async function generateMetadata( - { params }: { params: Params }, - parent: ResolvingMetadata, -): Promise { - const resolvedParams = await params; - const lesson = ( - await sanityFetch({ - query: lessonQuery, - params: resolvedParams, - stega: false, - tags: ["lesson"], - }) - ).data as LessonQueryResult; - const previousImages = (await parent).openGraph?.images || []; - const ogImage = resolveOpenGraphImage(lesson?.coverImage); - - return { - authors: - lesson?.author?.map((a) => { - return { name: a.title }; - }) || [], - title: lesson?.title, - description: lesson?.excerpt, - openGraph: { - images: ogImage ? ogImage : previousImages, - }, - } satisfies Metadata; -} - -export default async function LessonPage({ params }: { params: Params }) { - const resolvedParams = await params; - const [lesson, course] = ( - await Promise.all([ - sanityFetch({ query: lessonQuery, params: resolvedParams, tags: ["lesson"] }), - sanityFetch({ query: lessonsInCourseQuery, params: resolvedParams, tags: ["course", "lesson"] }), - ]) - ).map((res) => res.data) as [ - LessonQueryResult, - NonNullable, - ]; - - if (!lesson && !course) { - return notFound(); - } - - // Check if user is either a pro or paid for lesson - if (course?.stripeProduct && lesson?.locked) { - //First check if user session is valid - const cookieStore = await cookies(); - const sessionCookie = cookieStore.get("app.at"); - if (!sessionCookie) { - return redirect(`/course/${course?.slug}?showPro=true`); - } - } - - return ( - <> - {lesson?._id && course?._id && ( -
- Loading Lesson Panel...}> - - - {lesson?.content?.length && ( - - )} - -
- )} - - ); -} From f398fc732263094f78a89b92e1fb4afe3f840e6f Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:10 -0500 Subject: [PATCH 08/19] chore: remove course route file lessons.tsx --- .../(course)/course/[courseSlug]/lessons.tsx | 92 ------------------- 1 file changed, 92 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/lessons.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/lessons.tsx b/app/(main)/(course)/course/[courseSlug]/lessons.tsx deleted file mode 100644 index 806ee2bc..00000000 --- a/app/(main)/(course)/course/[courseSlug]/lessons.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import Link from "next/link"; - -import CoverImage from "@/components/cover-image"; -import type { LessonsInCourseQueryResult } from "@/sanity/types"; -import { sanityFetch } from "@/sanity/lib/live"; -import { lessonsInCourseQuery } from "@/sanity/lib/queries"; -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; - -export default async function Lessons(params: { courseSlug: string }) { - const { courseSlug } = await params; - const course = ( - await sanityFetch({ - query: lessonsInCourseQuery, - params: { courseSlug }, - tags: ["course", "lesson"], - }) - ).data as LessonsInCourseQueryResult; - - return ( - <> - {course?.sections && ( -
-
-

- Lessons -

- {course?.sections?.map((section) => ( -
-
-

{section?.title}

-
-
- {section?.lesson?.map((post) => { - const { - _id, - _type, - title, - slug, - coverImage, - excerpt, - locked, - } = post; - return ( - - - - - - - -

- - {title} - -

- - {excerpt && ( -

- {excerpt} -

- )} -
- - {locked && course?.stripeProduct && course?.title && ( -
- )} -
-
- ); - })} -
-
- ))} -
- )} - - ); -} From 70dfc1729d903d6914fd1246d233c61d1d3fa97c Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:11 -0500 Subject: [PATCH 09/19] chore: remove course route file page.tsx --- .../(course)/course/[courseSlug]/page.tsx | 140 ------------------ 1 file changed, 140 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/page.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/page.tsx b/app/(main)/(course)/course/[courseSlug]/page.tsx deleted file mode 100644 index 84a5881d..00000000 --- a/app/(main)/(course)/course/[courseSlug]/page.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import type { Metadata, ResolvingMetadata } from "next"; -import { groq, type PortableTextBlock } from "next-sanity"; -import { notFound } from "next/navigation"; -import { Suspense } from "react"; - -import Avatar from "@/components/avatar"; -import CoverMedia from "@/components/cover-media"; -import DateComponent from "@/components/date"; -import MoreContent from "@/components/more-content"; -import PortableText from "@/components/portable-text"; - -import type { CourseQueryResult } from "@/sanity/types"; -import { sanityFetch } from "@/sanity/lib/live"; -import { courseQuery } from "@/sanity/lib/queries"; -import { resolveOpenGraphImage } from "@/sanity/lib/utils"; -import Lessons from "./lessons"; -import MoreHeader from "@/components/more-header"; -import { BreadcrumbLinks } from "@/components/breadrumb-links"; -import Link from "next/link"; -import ShowPro from "./show-pro"; - -type Params = Promise<{ courseSlug: string }>; - -export const revalidate = 3600; - -export async function generateMetadata( - { params }: { params: Params }, - parent: ResolvingMetadata, -): Promise { - const { courseSlug } = await params; - const course = ( - await sanityFetch({ - query: courseQuery, - params: { courseSlug }, - stega: false, - tags: ["course", `course:${courseSlug}`], - }) - ).data as CourseQueryResult; - const previousImages = (await parent).openGraph?.images || []; - const ogImage = resolveOpenGraphImage(course?.coverImage); - - return { - authors: - course?.author?.map((a) => { - return { name: a.title }; - }) || [], - title: course?.title, - description: course?.excerpt, - openGraph: { - images: ogImage ? ogImage : previousImages, - }, - } satisfies Metadata; -} - -export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`*[_type == "course" && defined(slug.current)].slug.current`, - tags: ["course-list"], - stega: false, - }); - return (data as string[]).map((courseSlug) => ({ courseSlug })); -} - -export default async function CoursePage({ params }: { params: Params }) { - const { courseSlug } = await params; - - const course = ( - await sanityFetch({ - query: courseQuery, - params: { courseSlug }, - stega: false, - tags: ["course", `course:${courseSlug}`], - }) - ).data as CourseQueryResult; - - if (!course?._id) { - return notFound(); - } - - return ( -
- - -
-

- {course.title} -

-
- -
-
-
- {course.author && ( -
- {course.author.map((a) => ( - - ))} -
- )} -
-
- -
-
- {course?.stripeProduct && course?.title && ( -
- )} -
-
- {course.content?.length && ( - - )} -
-
- - - - -
- ); -} From a91afad3e881571be3b5031c38d8267d9fb239ff Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:12 -0500 Subject: [PATCH 10/19] chore: remove course route file show-pro.tsx --- .../(course)/course/[courseSlug]/show-pro.tsx | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/(main)/(course)/course/[courseSlug]/show-pro.tsx diff --git a/app/(main)/(course)/course/[courseSlug]/show-pro.tsx b/app/(main)/(course)/course/[courseSlug]/show-pro.tsx deleted file mode 100644 index 61acc049..00000000 --- a/app/(main)/(course)/course/[courseSlug]/show-pro.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import GoPro from "@/components/user-go-pro"; -import { usePathname, useSearchParams, useRouter } from "next/navigation"; -import { useState, useEffect } from "react"; - -export default function ShowPro() { - const [showGoPro, setShowGoPro] = useState(false); - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); - const showPro = searchParams.get("showPro"); - useEffect(() => { - if (showPro) { - router.replace(pathname); - setShowGoPro(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showPro]); - - return <>{showGoPro && }; -} From 8085d35fb172ac8b9d02b2ed0a9435a5715c9059 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:13 -0500 Subject: [PATCH 11/19] chore: remove course route file page.tsx --- .../(course)/courses/page/[num]/page.tsx | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 app/(main)/(course)/courses/page/[num]/page.tsx diff --git a/app/(main)/(course)/courses/page/[num]/page.tsx b/app/(main)/(course)/courses/page/[num]/page.tsx deleted file mode 100644 index 890869b9..00000000 --- a/app/(main)/(course)/courses/page/[num]/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import MoreContent from "@/components/more-content"; -import type { DocCountResult } from "@/sanity/types"; -import { sanityFetch } from "@/sanity/lib/live"; - -import PaginateList from "@/components/paginate-list"; -import { docCount } from "@/sanity/lib/queries"; -import { groq } from "next-sanity"; - -const LIMIT = 10; - -type Params = Promise<{ num: string }>; - -export const revalidate = 60; - -export async function generateStaticParams() { - const { data } = await sanityFetch({ - query: groq`count(*[_type == "course" && defined(slug.current)])`, - tags: ["course-list"], - stega: false, - }); - const count = data as number; - const perPage = LIMIT; - const pages = Math.ceil(count / perPage); - return Array.from({ length: pages }, (_, i) => ({ num: String(i + 1) })); -} - -export default async function Page({ params }: { params: Params }) { - const [count] = ( - await Promise.all([ - sanityFetch({ - query: docCount, - params: { - type: "course", - }, - tags: ["course-list", "course"], - }), - ]) - ).map((res) => res.data) as [DocCountResult]; - - const { num } = await params; - const pageNumber = Number(num); - const offset = (pageNumber - 1) * LIMIT; - const limit = offset + LIMIT; - - return ( -
- - -
- ); -} From d06c7cde7068e1d9d88289c95a178d2e4ed26984 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:14 -0500 Subject: [PATCH 12/19] chore: remove course route file page.tsx --- app/(main)/(course)/courses/page/page.tsx | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app/(main)/(course)/courses/page/page.tsx diff --git a/app/(main)/(course)/courses/page/page.tsx b/app/(main)/(course)/courses/page/page.tsx deleted file mode 100644 index f58ee0d1..00000000 --- a/app/(main)/(course)/courses/page/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default async function Page() { - redirect("/courses/page/1"); -} From 53ad7c2393fc8c704b73ae9fb77e5fef46ae2529 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:15 -0500 Subject: [PATCH 13/19] chore: remove course route file page.tsx --- app/(main)/(course)/courses/page.tsx | 97 ---------------------------- 1 file changed, 97 deletions(-) delete mode 100644 app/(main)/(course)/courses/page.tsx diff --git a/app/(main)/(course)/courses/page.tsx b/app/(main)/(course)/courses/page.tsx deleted file mode 100644 index 42580e14..00000000 --- a/app/(main)/(course)/courses/page.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import Link from "next/link"; -import { Suspense } from "react"; - -import Avatar from "@/components/avatar"; -import CoverImage from "@/components/cover-image"; -import DateComponent from "@/components/date"; -import MoreContent from "@/components/more-content"; -import Onboarding from "@/components/onboarding"; - -import type { CoursesQueryResult } from "@/sanity/types"; -import { sanityFetch } from "@/sanity/lib/live"; -import { coursesQuery } from "@/sanity/lib/queries"; -import MoreHeader from "@/components/more-header"; - -export const revalidate = 60; -function HeroCourse({ - title, - slug, - excerpt, - coverImage, - date, - author, -}: Pick< - Exclude, - "title" | "coverImage" | "date" | "excerpt" | "author" | "slug" ->) { - return ( -
- - - -
-
-

- - {title} - -

-
- -
-
-
- {excerpt && ( -

- {excerpt} -

- )} - {author && ( -
- {author.map((a) => ( - - ))} -
- )} -
-
-
- ); -} - -export default async function Page() { - const [heroPost] = ( - await Promise.all([sanityFetch({ query: coursesQuery, tags: ["course-list", "course"] })]) - ).map((res) => res.data) as [CoursesQueryResult]; - - return ( -
- {heroPost ? ( - - ) : ( - - )} - - {heroPost?._id && ( - - )} -
- ); -} From 1f7477143706caaedf8cde5c9e1211f3ee75bff8 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:16 -0500 Subject: [PATCH 14/19] chore: remove course route file route.ts --- app/(main)/(course)/courses/rss.json/route.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/(main)/(course)/courses/rss.json/route.ts diff --git a/app/(main)/(course)/courses/rss.json/route.ts b/app/(main)/(course)/courses/rss.json/route.ts deleted file mode 100644 index 2ccbe249..00000000 --- a/app/(main)/(course)/courses/rss.json/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const dynamic = "force-dynamic"; // defaults to auto - -import { buildFeed } from "@/lib/rss"; -import { ContentType } from "@/lib/types"; - -export async function GET() { - const feed = await buildFeed({ - type: ContentType.course, - }); - return new Response(feed.json1(), { - headers: { - "content-type": "application/json", - "cache-control": "max-age=0, s-maxage=3600", - }, - }); -} From 2dc1360919a024f468fe5cec20f7fe805315ec78 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:24:17 -0500 Subject: [PATCH 15/19] chore: remove course route file route.ts --- app/(main)/(course)/courses/rss.xml/route.ts | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/(main)/(course)/courses/rss.xml/route.ts diff --git a/app/(main)/(course)/courses/rss.xml/route.ts b/app/(main)/(course)/courses/rss.xml/route.ts deleted file mode 100644 index e0f1a1dd..00000000 --- a/app/(main)/(course)/courses/rss.xml/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const dynamic = "force-dynamic"; // defaults to auto - -import { buildFeed } from "@/lib/rss"; -import { ContentType } from "@/lib/types"; - -export async function GET() { - const feed = await buildFeed({ - type: ContentType.course, - }); - return new Response(feed.rss2(), { - headers: { - "content-type": "application/rss+xml; charset=utf-8", - "cache-control": "max-age=0, s-maxage=3600", - }, - }); -} From 632d49343a54e9dd53f44e1b3097c128effbf1ea Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 22:30:58 -0500 Subject: [PATCH 16/19] chore: remove course/lesson references from codebase Courses have been removed from navigation and are no longer active. Route files already deleted. This commit cleans up all remaining references: - queries.ts: remove course/lesson queries, partials, and related content refs - types.ts: remove course/lesson from ContentType enum - more-content.tsx: remove course query case - algolia-search.tsx: remove course/lesson icon cases - layout.tsx: remove courses RSS link - sitemap.ts: remove course/lesson URL generation - user-related.tsx: remove course from related content check - internalLink.ts: remove course from reference types Sanity schemas (course.ts, lesson.ts) kept for Studio access to existing data. --- app/(main)/layout.tsx | 1 - app/sitemap.ts | 11 +--- components/algolia-search.tsx | 6 --- components/more-content.tsx | 3 -- components/user-related.tsx | 1 - lib/types.ts | 13 ----- sanity/lib/queries.ts | 73 +-------------------------- sanity/schemas/custom/internalLink.ts | 1 - 8 files changed, 2 insertions(+), 107 deletions(-) diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 850c14a4..6875a702 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -74,7 +74,6 @@ export async function generateMetadata(): Promise { types: { "application/rss+xml": [ { url: "/blog/rss.xml", title: "Blog" }, - { url: "/courses/rss.xml", title: "Courses" }, { url: "/podcasts/rss.xml", title: "Podcasts" }, ], }, diff --git a/app/sitemap.ts b/app/sitemap.ts index c61ac2f4..dec59d51 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -39,17 +39,8 @@ export default async function sitemap(): Promise { url: `${site}${c._type === ContentType.page ? `/${c.slug}` : `/${c._type}/${c.slug}`}`, lastModified: new Date(), changeFrequency: "monthly", - priority: c._type === ContentType.course ? 0.8 : 0.5, + priority: 0.5, }); - c?.sections?.map((s) => - s?.lesson?.map((l) => { - sitemap.push({ - url: `${site}/course/${c.slug}/lesson/${l.slug}`, - lastModified: new Date(), - changeFrequency: "monthly", - }); - }), - ); } return sitemap; diff --git a/components/algolia-search.tsx b/components/algolia-search.tsx index abe42ccf..b365bf0a 100644 --- a/components/algolia-search.tsx +++ b/components/algolia-search.tsx @@ -6,8 +6,6 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { FaX } from "react-icons/fa6"; -import { GiTeacher } from "react-icons/gi"; // Course -import { PiStudentBold } from "react-icons/pi"; // Lesson import { FaPodcast } from "react-icons/fa"; // Podcast import { HiOutlinePencilAlt } from "react-icons/hi"; //Post import { FaCat } from "react-icons/fa"; // Author @@ -76,12 +74,8 @@ export default function AlgoliaSearch({ switch (type) { case ContentType.author: return ; - case ContentType.course: - return ; case ContentType.guest: return ; - case ContentType.lesson: - return ; case ContentType.podcast: return ; case ContentType.post: diff --git a/components/more-content.tsx b/components/more-content.tsx index 2e556c5b..ea73522e 100644 --- a/components/more-content.tsx +++ b/components/more-content.tsx @@ -13,7 +13,6 @@ import { sanityFetch } from "@/sanity/lib/live"; import { morePodcastQuery, morePostQuery, - moreCourseQuery, moreAuthorQuery, moreGuestQuery, moreSponsorQuery, @@ -32,8 +31,6 @@ export default async function MoreContent(params: { switch (params.type) { case ContentType.author: return moreAuthorQuery; - case ContentType.course: - return moreCourseQuery; case ContentType.guest: return moreGuestQuery; case ContentType.podcast: diff --git a/components/user-related.tsx b/components/user-related.tsx index 6d01edcb..2ded3bed 100644 --- a/components/user-related.tsx +++ b/components/user-related.tsx @@ -12,7 +12,6 @@ export default async function UserRelated( related: NonNullable["related"], ) { if ( - !related?.course?.length && !related?.podcast?.length && !related?.post?.length ) { diff --git a/lib/types.ts b/lib/types.ts index 908ef4af..c71b01c2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,4 @@ import type { - LessonsInCourseQueryResult, PageQueryResult, } from "@/sanity/types"; @@ -7,13 +6,11 @@ export type NonNull = Exclude; export enum ContentType { author = "author", - course = "course", framework = "framework", forum = "forum", guest = "guest", group = "group", language = "language", - lesson = "lesson", page = "page", podcast = "podcast", post = "post", @@ -40,13 +37,3 @@ export interface BookmarkPath extends BaseBookmarkContent { _cc_updated: number; } -export type BaseCompletedLesson = NonNullable< - NonNullable< - NonNullable< - NonNullable["sections"] - >[0]["lesson"] - >[0] ->; -export interface CompletedLesson extends BaseCompletedLesson { - _cc_updated: number; -} diff --git a/sanity/lib/queries.ts b/sanity/lib/queries.ts index ac5537ca..cf1390ce 100644 --- a/sanity/lib/queries.ts +++ b/sanity/lib/queries.ts @@ -73,14 +73,6 @@ const podcastFields = ` spotify `; -const courseFields = ` - stripeProduct -`; - -const lessonFields = ` - locked, - videoCloudinary -`; const userFields = ` socials, @@ -89,9 +81,6 @@ const userFields = ` const userRelated = ` "related":{ - "course": *[_type == "course" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] { - ${baseFieldsNoContent} - }, "podcast": *[_type == "podcast" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] { ${baseFieldsNoContent} }, @@ -103,9 +92,6 @@ const userRelated = ` const sponsorRelated = ` "related":{ - "course": *[_type == "course" && ^._id in sponsor[]._ref] | order(date desc) [] { - ${baseFieldsNoContent} - }, "podcast": *[_type == "podcast" && ^._id in sponsor[]._ref] | order(date desc) [] { ${baseFieldsNoContent} }, @@ -201,56 +187,6 @@ export const podcastQuery = groq`*[_type == "podcast" && slug.current == $slug] ${contentFields}, ${podcastFields} }`; - -// Courses - -export const coursesQuery = groq`*[_type == "course" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] { - ${baseFieldsNoContent}, - ${courseFields}, - author[]->{ - ..., - "title": coalesce(title, "Anonymous"), - "slug": slug.current, - } -}`; - -export const moreCourseQuery = groq`*[_type == "course" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [$offset...$limit] { - ${baseFieldsNoContent}, - ${courseFields}, - author[]->{ - ..., - "title": coalesce(title, "Anonymous"), - "slug": slug.current, - } -}`; - -export const courseQuery = groq`*[_type == "course" && slug.current == $courseSlug] [0] { - ${baseFieldsNoContent}, - ${courseFields}, - ${contentFields}, - ${podcastFields} -}`; - -// Lessons - -export const lessonsInCourseQuery = groq`*[_type == "course" && slug.current == $courseSlug] [0] { - ${baseFieldsNoContent}, - ${courseFields}, - sections[]{ - title, - lesson[]->{ - ${baseFieldsNoContent}, - ${lessonFields} - } - } -}`; - -export const lessonQuery = groq`*[_type == "lesson" && slug.current == $lessonSlug] [0] { - ${baseFieldsNoContent}, - ${contentFields}, - ${lessonFields} -}`; - // Author export const moreAuthorQuery = groq`*[_type == "author" && _id != $skip && defined(slug.current)] | order(title) [$offset...$limit] { @@ -322,15 +258,8 @@ export const rssPodcastQuery = groq`*[_type == "podcast" && _id != $skip && defi }`; // Sitemaps -export const sitemapQuery = groq`*[_type in ["author", "course", "guest", "page", "podcast", "post", "sponsor"] && defined(slug.current)] | order(_type asc) | order(_updated desc) { +export const sitemapQuery = groq`*[_type in ["author", "guest", "page", "podcast", "post", "sponsor"] && defined(slug.current)] | order(_type asc) | order(_updated desc) { _type, _updatedAt, "slug": slug.current, - sections[]{ - lesson[]->{ - _type, - _updatedAt, - "slug": slug.current, - } - } }`; diff --git a/sanity/schemas/custom/internalLink.ts b/sanity/schemas/custom/internalLink.ts index 73d2451a..065fc37c 100644 --- a/sanity/schemas/custom/internalLink.ts +++ b/sanity/schemas/custom/internalLink.ts @@ -14,7 +14,6 @@ export default defineType({ to: [ { type: "post" }, { type: "podcast" }, - { type: "course" }, { type: "page" }, // other types you may want to link to ], From 10cc23f9055e08bd6708eb757b701875377994ab Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 23:09:56 -0500 Subject: [PATCH 17/19] chore: delete course schema --- sanity/schemas/documents/course.ts | 61 ------------------------------ 1 file changed, 61 deletions(-) delete mode 100644 sanity/schemas/documents/course.ts diff --git a/sanity/schemas/documents/course.ts b/sanity/schemas/documents/course.ts deleted file mode 100644 index fe1fb0ad..00000000 --- a/sanity/schemas/documents/course.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { GiTeacher } from "react-icons/gi"; -import { format, parseISO } from "date-fns"; -import { defineField, defineType } from "sanity"; - -import contentType from "../partials/content"; -import lessonType from "./lesson"; - -export default defineType({ - ...contentType, - name: "course", - title: "Course", - icon: GiTeacher, - type: "document", - groups: [ - ...(contentType.groups || []), - { - name: "sections", - title: "Sections / Lessons", - default: true, - }, - ], - fields: [ - ...contentType.fields, - defineField({ - name: "stripeProduct", - title: "Stripe Product Id", - type: "string", - }), - defineField({ - name: "sections", - title: "Sections", - type: "array", - group: "sections", - of: [ - { - name: "section", - title: "Section", - type: "object", - fields: [ - defineField({ - name: "title", - title: "Title", - type: "string", - }), - defineField({ - name: "lesson", - title: "Lessons", - type: "array", - of: [ - { - type: "reference", - to: [{ type: lessonType.name }], - }, - ], - }), - ], - }, - ], - }), - ], -}); From 7b285ad0453503b8beaf6ed6fe31ec79ac5daf60 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 23:09:57 -0500 Subject: [PATCH 18/19] chore: delete lesson schema --- sanity/schemas/documents/lesson.ts | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 sanity/schemas/documents/lesson.ts diff --git a/sanity/schemas/documents/lesson.ts b/sanity/schemas/documents/lesson.ts deleted file mode 100644 index 38ca93be..00000000 --- a/sanity/schemas/documents/lesson.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { PiStudentBold } from "react-icons/pi"; -import { format, parseISO } from "date-fns"; -import { defineField, defineType } from "sanity"; - -import contentType from "../partials/content"; - -export default defineType({ - ...contentType, - name: "lesson", - title: "Lesson", - icon: PiStudentBold, - type: "document", - fields: [ - ...contentType.fields, - defineField({ - name: "locked", - title: "Locked", - type: "boolean", - }), - ], -}); From f25da916dfc3ad9d7feb3e4290e14d215d601878 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 23:09:59 -0500 Subject: [PATCH 19/19] chore: remove all remaining course/lesson references - pro-benefits.tsx: replace course marketing copy with generic content terms - rss.ts: remove dead course case from typePath() - algolia-search.tsx: remove commented-out course initialUiState - sanity.config.ts: remove course/lesson schema imports and registrations - Deleted: sanity/schemas/documents/course.ts, lesson.ts --- components/algolia-search.tsx | 7 ------- components/pro-benefits.tsx | 10 +++++----- lib/rss.ts | 2 -- sanity.config.ts | 4 ---- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/components/algolia-search.tsx b/components/algolia-search.tsx index b365bf0a..6abcdff4 100644 --- a/components/algolia-search.tsx +++ b/components/algolia-search.tsx @@ -139,13 +139,6 @@ export default function AlgoliaSearch({ future={{ preserveSharedStateOnUnmount: true, }} - // initialUiState={{ - // [indexName]: { - // refinementList: { - // _type: ["course"], - // }, - // }, - // }} >
{showFacets && ( diff --git a/components/pro-benefits.tsx b/components/pro-benefits.tsx index a00ee545..30427af8 100644 --- a/components/pro-benefits.tsx +++ b/components/pro-benefits.tsx @@ -47,7 +47,7 @@ export default function ProBenefits({

Unlock premium benefits with our CodingCat.dev Pro plan, - including advanced courses, lifetime access, and personalized + including premium content, lifetime access, and personalized support.

@@ -80,15 +80,15 @@ export default function ProBenefits({

As a CodingCat.dev Pro member, you'll gain access to our - advanced course library, covering topics like machine learning, - data science, and cloud architecture. + premium content library, covering topics like web development, + cloud architecture, and modern frameworks.

- Explore Courses + Explore Content
diff --git a/lib/rss.ts b/lib/rss.ts index 283b962a..7b83e76c 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -17,8 +17,6 @@ function typePath(type: string): string { return "blog"; case "podcast": return "podcasts"; - case "course": - return "courses"; default: return type + "s"; } diff --git a/sanity.config.ts b/sanity.config.ts index ee5d433f..95075aa9 100644 --- a/sanity.config.ts +++ b/sanity.config.ts @@ -35,8 +35,6 @@ import { sharePreviewAction } from "@/sanity/components/documentActions/sharePre import { assistWithPresets } from "@/sanity/plugins/assist"; import author from "@/sanity/schemas/documents/author"; import previewSession from "@/sanity/schemas/previewSession"; -import course from "@/sanity/schemas/documents/course"; -import lesson from "@/sanity/schemas/documents/lesson"; import guest from "@/sanity/schemas/documents/guest"; import page from "@/sanity/schemas/documents/page"; import podcast from "@/sanity/schemas/documents/podcast"; @@ -142,8 +140,6 @@ export default defineConfig({ dashboardSettings, // Documents author, - course, - lesson, guest, page, podcast,