Skip to content

Commit 2ee0311

Browse files
committed
fix(site): always prioritize coverImage and ogImage as primary social cards across all courses, lessons, podcasts, posts, and pages
1 parent 1a37e63 commit 2ee0311

5 files changed

Lines changed: 164 additions & 62 deletions

File tree

‎apps/site/src/pages/[slug].astro‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ if (!page?._id) {
1515
1616
const path = `/${slug}`;
1717
const origin = Astro.locals.siteUrl.origin;
18-
const ogImageOverride = page.ogImage
18+
const ogSocialImage = page.ogImage
1919
? sanity.resolveOpenGraphImage(page.ogImage, 1200, 630)
20-
: undefined;
20+
: page.coverImage
21+
? sanity.resolveOpenGraphImage(page.coverImage, 1200, 630)
22+
: undefined;
2123
const ogCardImageUrl = sanity
2224
.urlForImage(page.coverImage ?? undefined)
2325
?.width(640)
@@ -29,7 +31,7 @@ const ogCardImageUrl = sanity
2931
<BaseLayout
3032
title={page.title}
3133
description={page.excerpt}
32-
image={ogImageOverride}
34+
image={ogSocialImage}
3335
ogFallback={{
3436
title: page.title,
3537
subtitle: page.excerpt,

‎apps/site/src/pages/course/[slug].astro‎

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
/**
33
* `/course/[slug]` | Individual Course Detail & Lessons Page.
44
*/
5+
import Avatar from "@/components/Avatar.astro";
56
import Breadcrumbs from "@/components/Breadcrumbs.astro";
7+
import ContentGrid from "@/components/ContentGrid.astro";
68
import CoverMedia from "@/components/CoverMedia.astro";
79
import ExcalidrawBlueprintCard from "@/components/ExcalidrawBlueprintCard.astro";
810
import PortableTextContent from "@/components/portable-text/PortableTextContent.astro";
11+
import SectionHeader from "@/components/SectionHeader.astro";
12+
import TagPills from "@/components/TagPills.astro";
913
import 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";
1120
import type { CoverImage as CoverImageType } from "@/lib/sanity/types.gen";
21+
import { matchTopicsForText } from "@/lib/topics";
1222
1323
interface 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
2233
interface 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+
5486
const sections: CourseSection[] = course.sections ?? [];
5587
const allLessons: CourseLesson[] = sections.flatMap((sec) =>
5688
(sec?.lesson ?? []).filter((l): l is CourseLesson => Boolean(l?.slug)),
5789
);
5890
const 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
6599
const 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>

‎apps/site/src/pages/course/[slug]/lesson/[lessonSlug].astro‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
* `/course/[slug]/lesson/[lessonSlug]` | Interactive Course Lesson Player & Tutorial Page.
44
*/
55
import Breadcrumbs from "@/components/Breadcrumbs.astro";
6+
import ContentGrid from "@/components/ContentGrid.astro";
67
import CoverMedia from "@/components/CoverMedia.astro";
78
import ExcalidrawBlueprintCard from "@/components/ExcalidrawBlueprintCard.astro";
89
import PortableTextContent from "@/components/portable-text/PortableTextContent.astro";
10+
import SectionHeader from "@/components/SectionHeader.astro";
11+
import TagPills from "@/components/TagPills.astro";
912
import BaseLayout from "@/layouts/BaseLayout.astro";
10-
import { courseQuery } from "@/lib/sanity/queries";
13+
import type { ContentListItem } from "@/lib/content";
14+
import {
15+
type RelatedCandidate,
16+
resolveRelatedContentList,
17+
} from "@/lib/related-content";
18+
import { courseQuery, relatedCandidatesQuery } from "@/lib/sanity/queries";
1119
import type { CoverImage as CoverImageType } from "@/lib/sanity/types.gen";
20+
import { matchTopicsForText } from "@/lib/topics";
1221
1322
interface CourseLesson {
1423
_id?: string;
@@ -18,6 +27,8 @@ interface CourseLesson {
1827
coverImage?: CoverImageType | null;
1928
videoCloudinary?: { asset?: { _ref?: string } } | null;
2029
youtube?: string | null;
30+
tags?: Array<{ label?: string | null; value?: string | null }> | null;
31+
relatedContent?: RelatedCandidate[] | null;
2132
content?: Array<{
2233
_type: string;
2334
children?: Array<{ text?: string }>;
@@ -38,6 +49,8 @@ interface CourseDetail {
3849
coverImage?: CoverImageType | null;
3950
videoCloudinary?: { asset?: { _ref?: string } } | null;
4051
youtube?: string | null;
52+
tags?: Array<{ label?: string | null; value?: string | null }> | null;
53+
relatedContent?: RelatedCandidate[] | null;
4154
content?: Array<{
4255
_type: string;
4356
children?: Array<{ text?: string }>;
@@ -110,11 +123,38 @@ const lessonTitle =
110123
currentLesson.title ||
111124
`Lesson ${currentEntry.sectionIndex}.${currentEntry.lessonIndex}`;
112125
const lessonDescription = currentLesson.excerpt || course.excerpt || "";
126+
const candidates = ((await sanity.loadQuery(relatedCandidatesQuery, {
127+
skip: course._id,
128+
})) ?? []) as unknown as RelatedCandidate[];
129+
const matchedTopics = matchTopicsForText(
130+
`${lessonTitle} ${lessonDescription} ${course.title ?? ""}`,
131+
);
132+
const more = resolveRelatedContentList(
133+
{
134+
_id: currentLesson._id || course._id,
135+
title: lessonTitle,
136+
excerpt: lessonDescription,
137+
tags: currentLesson.tags ?? course.tags,
138+
relatedContent: currentLesson.relatedContent ?? course.relatedContent,
139+
},
140+
candidates,
141+
{
142+
limit: 2,
143+
preferSameType: false,
144+
currentTopics: matchedTopics,
145+
},
146+
);
147+
148+
const lessonCover = currentLesson.coverImage ?? course.coverImage;
149+
const ogSocialImage = lessonCover
150+
? sanity.resolveOpenGraphImage(lessonCover, 1200, 630)
151+
: undefined;
113152
---
114153

115154
<BaseLayout
116155
title={`${lessonTitle} | ${course.title} | CodingCat.dev Course`}
117156
description={lessonDescription}
157+
image={ogSocialImage}
118158
ogFallback={{
119159
title: lessonTitle,
120160
subtitle: `${course.title} · Lesson ${currentEntry.globalIndex} of ${indexedLessons.length}`,
@@ -143,6 +183,12 @@ const lessonDescription = currentLesson.excerpt || course.excerpt || "";
143183
<span class="rounded-full border-2 border-[#2A1155]/30 bg-white/80 px-3 py-1 font-mono text-xs font-bold text-[#2A1155] dark:border-white/20 dark:bg-[#131B2E] dark:text-slate-200">
144184
Section {currentEntry.sectionIndex}.{currentEntry.lessonIndex} · Lesson {currentEntry.globalIndex} of {indexedLessons.length}
145185
</span>
186+
<TagPills
187+
title={`${lessonTitle} ${course.title ?? ""}`}
188+
excerpt={lessonDescription}
189+
tags={currentLesson.tags ?? course.tags}
190+
size="md"
191+
/>
146192
</div>
147193

148194
<div class="flex items-center gap-2">
@@ -268,6 +314,16 @@ const lessonDescription = currentLesson.excerpt || course.excerpt || "";
268314
)
269315
}
270316
</nav>
317+
318+
{
319+
more.length > 0 && (
320+
<div class="mt-12">
321+
<hr class="my-8 border-t-2 border-dashed border-[#2A1155]/25 dark:border-white/15" />
322+
<SectionHeader title="related deep dives" href="/blog" />
323+
<ContentGrid items={more as unknown as ContentListItem[]} />
324+
</div>
325+
)
326+
}
271327
</div>
272328

273329
<!-- Sticky Course Lessons Sidebar -->

‎apps/site/src/pages/podcast/[slug].astro‎

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import PortableTextContent from "@/components/portable-text/PortableTextContent.
1111
import PtYouTube from "@/components/portable-text/PtYouTube.astro";
1212
import SectionHeader from "@/components/SectionHeader.astro";
1313
import SponsorCard from "@/components/SponsorCard.astro";
14+
import TagPills from "@/components/TagPills.astro";
1415
import TranscriptModal from "@/components/TranscriptModal.astro";
1516
import BaseLayout from "@/layouts/BaseLayout.astro";
1617
import type { ContentListItem } from "@/lib/content";
@@ -62,9 +63,11 @@ const origin = Astro.locals.siteUrl.origin;
6263
const coverThumbnail = sanity.resolveOpenGraphImage(
6364
podcast.coverImage ?? undefined,
6465
);
65-
const ogImageOverride = podcast.ogImage
66+
const ogSocialImage = podcast.ogImage
6667
? sanity.resolveOpenGraphImage(podcast.ogImage, 1200, 630)
67-
: undefined;
68+
: podcast.coverImage
69+
? sanity.resolveOpenGraphImage(podcast.coverImage, 1200, 630)
70+
: undefined;
6871
const ogCardImageUrl = sanity
6972
.urlForImage(podcast.coverImage ?? undefined)
7073
?.width(640)
@@ -113,7 +116,7 @@ const faqNode = faqSchema(extractFaqsFromPortableText(podcast.content));
113116
<BaseLayout
114117
title={podcast.title}
115118
description={podcast.excerpt}
116-
image={ogImageOverride}
119+
image={ogSocialImage}
117120
ogFallback={{
118121
title: podcast.title,
119122
subtitle: podcast.excerpt,
@@ -204,30 +207,12 @@ const faqNode = faqSchema(extractFaqsFromPortableText(podcast.content));
204207
<FormattedDate dateString={podcast.date} />
205208
</span>
206209
)}
207-
{matchedTopics.map((topic) => (
208-
<a
209-
href={`/topic/${topic.slug}`}
210-
class="inline-flex items-center gap-2 rounded-full border-2 border-[#2A1155] bg-white px-3.5 py-1 font-display text-xs font-black text-[#2A1155] transition-all hover:-translate-y-0.5 hover:bg-[#EDE9FE] hover:text-[#7C3AED] dark:border-white/25 dark:bg-[#131B2E] dark:text-white dark:hover:bg-purple-950/60 shadow-sketch-sm"
211-
>
212-
{topic.symbolLight && (
213-
<img
214-
src={topic.symbolLight}
215-
alt=""
216-
aria-hidden="true"
217-
class="h-3.5 w-auto object-contain dark:hidden"
218-
/>
219-
)}
220-
{topic.symbolDark && (
221-
<img
222-
src={topic.symbolDark}
223-
alt=""
224-
aria-hidden="true"
225-
class="hidden h-3.5 w-auto object-contain dark:block"
226-
/>
227-
)}
228-
<span>{topic.title}</span>
229-
</a>
230-
))}
210+
<TagPills
211+
title={podcast.title}
212+
excerpt={podcast.excerpt}
213+
tags={podcastWithRelated.tags}
214+
size="md"
215+
/>
231216
</div>
232217

233218
<h1 class="mb-8 break-words font-display text-3xl font-black leading-tight tracking-tight text-balance text-[#2A1155] dark:text-white sm:text-4xl md:text-6xl">

0 commit comments

Comments
 (0)