22/**
33 * `/course/[slug]` | Individual Course Detail & Lessons Page.
44 */
5+ import Avatar from " @/components/Avatar.astro" ;
56import Breadcrumbs from " @/components/Breadcrumbs.astro" ;
7+ import ContentGrid from " @/components/ContentGrid.astro" ;
68import CoverMedia from " @/components/CoverMedia.astro" ;
79import ExcalidrawBlueprintCard from " @/components/ExcalidrawBlueprintCard.astro" ;
810import PortableTextContent from " @/components/portable-text/PortableTextContent.astro" ;
11+ import SectionHeader from " @/components/SectionHeader.astro" ;
12+ import TagPills from " @/components/TagPills.astro" ;
913import BaseLayout from " @/layouts/BaseLayout.astro" ;
10- import { courseQuery } from " @/lib/sanity/queries" ;
14+ import type { ContentListItem } from " @/lib/content" ;
15+ import {
16+ type RelatedCandidate ,
17+ resolveRelatedContentList ,
18+ } from " @/lib/related-content" ;
19+ import { courseQuery , relatedCandidatesQuery } from " @/lib/sanity/queries" ;
1120import type { CoverImage as CoverImageType } from " @/lib/sanity/types.gen" ;
21+ import { matchTopicsForText } from " @/lib/topics" ;
1222
1323interface CourseLesson {
1424 _id? : string ;
@@ -17,6 +27,7 @@ interface CourseLesson {
1727 excerpt? : string | null ;
1828 youtube? : string | null ;
1929 videoCloudinary? : { asset? : { _ref? : string } } | null ;
30+ tags? : Array <{ label? : string | null ; value? : string | null }> | null ;
2031}
2132
2233interface CourseSection {
@@ -33,6 +44,14 @@ interface CourseDetail {
3344 ogImage? : CoverImageType | null ;
3445 videoCloudinary? : { asset? : { _ref? : string } } | null ;
3546 youtube? : string | null ;
47+ author? : Array <{
48+ _id: string ;
49+ title: string ;
50+ slug? : string | null ;
51+ coverImage? : CoverImageType | null ;
52+ }> | null ;
53+ tags? : Array <{ label? : string | null ; value? : string | null }> | null ;
54+ relatedContent? : RelatedCandidate [] | null ;
3655 content? : Array <{
3756 _type: string ;
3857 children? : Array <{ text? : string }>;
@@ -51,15 +70,30 @@ if (!course?._id) {
5170 return new Response (null , { status: 404 });
5271}
5372
73+ const candidates = ((await sanity .loadQuery (relatedCandidatesQuery , {
74+ skip: course ._id ,
75+ })) ?? []) as unknown as RelatedCandidate [];
76+ const matchedTopics = matchTopicsForText (
77+ ` ${course .title ?? " " } ${course .excerpt ?? " " } ` ,
78+ );
79+ const more = resolveRelatedContentList (course , candidates , {
80+ limit: 2 ,
81+ preferSameType: false ,
82+ currentTopics: matchedTopics ,
83+ });
84+ const authors = course .author ?? [];
85+
5486const sections: CourseSection [] = course .sections ?? [];
5587const allLessons: CourseLesson [] = sections .flatMap ((sec ) =>
5688 (sec ?.lesson ?? []).filter ((l ): l is CourseLesson => Boolean (l ?.slug )),
5789);
5890const firstLesson = allLessons [0 ] ?? null ;
5991
60- const ogImageOverride = course .ogImage
92+ const ogSocialImage = course .ogImage
6193 ? sanity .resolveOpenGraphImage (course .ogImage , 1200 , 630 )
62- : undefined ;
94+ : course .coverImage
95+ ? sanity .resolveOpenGraphImage (course .coverImage , 1200 , 630 )
96+ : undefined ;
6397
6498// Strip legacy blocks mentioning contacting @BrittneyPostma or @codercatdev
6599const sanitizedContent = (course .content ?? []).filter ((block ) => {
@@ -73,7 +107,7 @@ const sanitizedContent = (course.content ?? []).filter((block) => {
73107<BaseLayout
74108 title ={ ` ${course .title } | CodingCat.dev Course ` }
75109 description ={ course .excerpt }
76- image ={ ogImageOverride }
110+ image ={ ogSocialImage }
77111 ogFallback ={ {
78112 title: course .title ,
79113 subtitle: course .excerpt ,
@@ -103,6 +137,12 @@ const sanitizedContent = (course.content ?? []).filter((block) => {
103137 </span >
104138 )
105139 }
140+ <TagPills
141+ title ={ course .title }
142+ excerpt ={ course .excerpt }
143+ tags ={ course .tags }
144+ size =" md"
145+ />
106146 </div >
107147 <div class =" flex flex-wrap items-center gap-2.5" >
108148 <a
@@ -142,6 +182,20 @@ const sanitizedContent = (course.content ?? []).filter((block) => {
142182 />
143183 </div >
144184
185+ {
186+ authors .length > 0 && (
187+ <div class = " mb-8 flex flex-wrap gap-2.5" >
188+ { authors .map ((author ) => (
189+ <Avatar
190+ name = { author .title }
191+ href = { author .slug ? ` /author/${author .slug } ` : undefined }
192+ coverImage = { author .coverImage }
193+ />
194+ ))}
195+ </div >
196+ )
197+ }
198+
145199 {
146200 sanitizedContent .length > 0 && (
147201 <div class = " my-8" >
@@ -199,6 +253,16 @@ const sanitizedContent = (course.content ?? []).filter((block) => {
199253 { lesson .excerpt }
200254 </p >
201255 )}
256+ <div class = " mt-2 flex flex-wrap items-center gap-1" >
257+ <TagPills
258+ as = " span"
259+ title = { lesson ?.title }
260+ excerpt = { lesson ?.excerpt }
261+ tags = { lesson ?.tags }
262+ maxCount = { 3 }
263+ size = " sm"
264+ />
265+ </div >
202266 </div >
203267 </div >
204268 <div class = " flex items-center justify-between border-t border-[#2A1155]/10 pt-2.5 font-mono text-[11px] font-bold text-[#7C3AED] dark:border-white/10 dark:text-[#C084FC]" >
@@ -238,4 +302,14 @@ const sanitizedContent = (course.content ?? []).filter((block) => {
238302 tags ={ sections .map ((s ) => s ?.title )}
239303 />
240304 </article >
305+
306+ {
307+ more .length > 0 && (
308+ <aside class = " mx-auto max-w-4xl" >
309+ <hr class = " my-12 border-t-2 border-dashed border-[#2A1155]/25 dark:border-white/15" />
310+ <SectionHeader title = " related deep dives" href = " /blog" />
311+ <ContentGrid items = { more as unknown as ContentListItem []} />
312+ </aside >
313+ )
314+ }
241315</BaseLayout >
0 commit comments