Episode highlights per series: tag by lane, group on Episodes page, move to Finished Book when series ends; v2.9.1
Publish Container / build (push) Successful in 15m21s

- Highlights get a Series field (Lane A / Lane B / General); legacy highlights count as Lane A
- Episodes page groups highlights under each series' title
- Ending a series (scheduled or manual) moves only that series' highlights onto its Finished Book page; other lane and General stay
- Archived highlights keep working: detail pages, sitemap, scripture index and search include them

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-08 10:46:33 -04:00
1 parent ddcaec0ddf
commit 652b003bd9
9 files changed
+133 -57

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "siteforge", "name": "siteforge",
"private": true, "private": true,
"version": "2.9.0", "version": "2.9.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1
View File
@@ -141,6 +141,7 @@ export function sanitizeFeaturedLinks(value) {
return { return {
id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(), id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
lane: item.lane === 'second' || item.lane === 'general' ? item.lane : 'primary',
title: typeof item.title === 'string' ? item.title.trim().slice(0, 140) : '', title: typeof item.title === 'string' ? item.title.trim().slice(0, 140) : '',
episodeNumber: typeof item.episodeNumber === 'string' ? item.episodeNumber.trim().slice(0, 20) : '', episodeNumber: typeof item.episodeNumber === 'string' ? item.episodeNumber.trim().slice(0, 20) : '',
summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '', summary: typeof item.summary === 'string' ? item.summary.trim().slice(0, 600) : '',
+8 -6
View File
@@ -82,6 +82,12 @@ function applySeriesTransition(content, sched, lane = 'primary') {
const newSeason = Number.isInteger(sched.newSeason) ? sched.newSeason : undefined const newSeason = Number.isInteger(sched.newSeason) ? sched.newSeason : undefined
const newStudySlug = typeof sched.newStudySlug === 'string' ? sched.newStudySlug.trim().toLowerCase() : '' const newStudySlug = typeof sched.newStudySlug === 'string' ? sched.newStudySlug.trim().toLowerCase() : ''
// Highlights for the ending series move onto its Finished Book; the other lane's and general ones stay.
const allHighlights = Array.isArray(content.podcastFeaturedLinks) ? content.podcastFeaturedLinks : []
const isThisLane = link => (link.lane === 'second' ? 'second' : link.lane === 'general' ? 'general' : 'primary') === lane
const endingHighlights = current.title.trim() ? allHighlights.filter(isThisLane) : []
const keptHighlights = current.title.trim() ? allHighlights.filter(link => !isThisLane(link)) : allHighlights
const archivedBook = current.title.trim() const archivedBook = current.title.trim()
? { ? {
id: Date.now().toString(36), id: Date.now().toString(36),
@@ -90,6 +96,7 @@ function applySeriesTransition(content, sched, lane = 'primary') {
imageUrl: current.imageUrl, imageUrl: current.imageUrl,
season: current.season ?? ((newSeason ?? 2) - 1 || 1), season: current.season ?? ((newSeason ?? 2) - 1 || 1),
...(current.studySlug ? { studySlug: current.studySlug } : {}), ...(current.studySlug ? { studySlug: current.studySlug } : {}),
...(endingHighlights.length > 0 ? { highlights: endingHighlights.map(({ lane: _lane, ...link }) => link) } : {}),
} }
: null : null
@@ -126,10 +133,6 @@ function applySeriesTransition(content, sched, lane = 'primary') {
return study return study
}) })
const hasOtherLane = lane === 'second'
? Boolean(content.seriesTitle?.trim())
: Boolean(content.secondSeries?.title?.trim())
const lanePatch = lane === 'second' const lanePatch = lane === 'second'
? { ? {
secondSeries: { secondSeries: {
@@ -155,8 +158,7 @@ function applySeriesTransition(content, sched, lane = 'primary') {
...content, ...content,
...lanePatch, ...lanePatch,
studies, studies,
// Highlights belong to whatever is playing now; don't wipe them while the other lane is still running. podcastFeaturedLinks: keptHighlights,
podcastFeaturedLinks: hasOtherLane ? content.podcastFeaturedLinks : [],
finishedBooks: [...(Array.isArray(content.finishedBooks) ? content.finishedBooks : []), ...(archivedBook ? [archivedBook] : [])], finishedBooks: [...(Array.isArray(content.finishedBooks) ? content.finishedBooks : []), ...(archivedBook ? [archivedBook] : [])],
...patch, ...patch,
}) })
+8 -2
View File
@@ -102,7 +102,10 @@ export function register(app) {
} }
} }
// Dynamic episode highlight URLs // Dynamic episode highlight URLs
const highlights = Array.isArray(state.cachedSiteContent?.podcastFeaturedLinks) ? state.cachedSiteContent.podcastFeaturedLinks : [] const highlights = [
...(Array.isArray(state.cachedSiteContent?.podcastFeaturedLinks) ? state.cachedSiteContent.podcastFeaturedLinks : []),
...(Array.isArray(state.cachedSiteContent?.finishedBooks) ? state.cachedSiteContent.finishedBooks.flatMap(book => (Array.isArray(book?.highlights) ? book.highlights : [])) : []),
]
for (const ep of highlights) { for (const ep of highlights) {
if (ep.id) urls.push(`${root}/episodes/${encodeURIComponent(ep.id)}`) if (ep.id) urls.push(`${root}/episodes/${encodeURIComponent(ep.id)}`)
} }
@@ -195,7 +198,10 @@ export function register(app) {
app.get('/api/public/scripture-index', async (_req, res) => { app.get('/api/public/scripture-index', async (_req, res) => {
const content = state.cachedSiteContent const content = state.cachedSiteContent
const allStudies = Array.isArray(content?.studies) ? content.studies : [] const allStudies = Array.isArray(content?.studies) ? content.studies : []
const allEpisodes = Array.isArray(content?.podcastFeaturedLinks) ? content.podcastFeaturedLinks : [] const allEpisodes = [
...(Array.isArray(content?.podcastFeaturedLinks) ? content.podcastFeaturedLinks : []),
...(Array.isArray(content?.finishedBooks) ? content.finishedBooks.flatMap(book => (Array.isArray(book?.highlights) ? book.highlights : [])) : []),
]
const cutoff = new Date() const cutoff = new Date()
const entries = [] const entries = []
+31 -1
View File
@@ -14,6 +14,20 @@ import InboxPanel, { type InboxSubmission } from './InboxPanel'
import AssetManager, { type AdminAsset } from './AssetManager' import AssetManager, { type AdminAsset } from './AssetManager'
type HighlightLane = 'primary' | 'second' | 'general'
function highlightLaneOf(link: PodcastFeaturedLink): HighlightLane {
return link.lane === 'second' || link.lane === 'general' ? link.lane : 'primary'
}
// Splits highlights into the ones belonging to a series (moved onto its Finished Book) and the rest (kept).
function splitHighlightsForLane(links: PodcastFeaturedLink[] | undefined, lane: HighlightLane) {
const all = links ?? []
const ending = all.filter(link => highlightLaneOf(link) === lane).map(({ lane: _lane, ...link }) => link)
const kept = all.filter(link => highlightLaneOf(link) !== lane)
return { ending, kept }
}
function toLocalInputValue(iso?: string): string { function toLocalInputValue(iso?: string): string {
if (!iso) return '' if (!iso) return ''
const d = new Date(iso) const d = new Date(iso)
@@ -2216,6 +2230,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
...(f.podcastFeaturedLinks ?? []), ...(f.podcastFeaturedLinks ?? []),
{ {
id: Date.now().toString(36), id: Date.now().toString(36),
lane: 'primary' as const,
title: '', title: '',
episodeNumber: '', episodeNumber: '',
summary: '', summary: '',
@@ -4154,6 +4169,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
setForm(f => { setForm(f => {
const cur = f.secondSeries const cur = f.secondSeries
if (!cur?.title?.trim()) return f if (!cur?.title?.trim()) return f
const { ending: endingHighlights, kept: keptHighlights } = splitHighlightsForLane(f.podcastFeaturedLinks, 'second')
const archived = { const archived = {
id: Date.now().toString(36), id: Date.now().toString(36),
title: cur.title, title: cur.title,
@@ -4161,9 +4177,11 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
imageUrl: cur.imageUrl, imageUrl: cur.imageUrl,
season: cur.season ?? 1, season: cur.season ?? 1,
...(cur.studySlug ? { studySlug: cur.studySlug } : {}), ...(cur.studySlug ? { studySlug: cur.studySlug } : {}),
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
} }
return { return {
...f, ...f,
podcastFeaturedLinks: keptHighlights,
finishedBooks: [...(f.finishedBooks ?? []), archived], finishedBooks: [...(f.finishedBooks ?? []), archived],
studies: (f.studies ?? []).map(st => st.slug === cur.studySlug && st.status !== 'planned' ? { ...st, status: 'complete' as const } : st), studies: (f.studies ?? []).map(st => st.slug === cur.studySlug && st.status !== 'planned' ? { ...st, status: 'complete' as const } : st),
secondSeries: undefined, secondSeries: undefined,
@@ -4207,6 +4225,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
className="btn-primary" className="btn-primary"
disabled={!endSeriesNewTitle.trim() || !endSeriesNewSeason.trim()} disabled={!endSeriesNewTitle.trim() || !endSeriesNewSeason.trim()}
onClick={() => { onClick={() => {
const { ending: endingHighlights, kept: keptHighlights } = splitHighlightsForLane(form.podcastFeaturedLinks, 'primary')
const archivedBook = { const archivedBook = {
id: Date.now().toString(36), id: Date.now().toString(36),
title: form.seriesTitle, title: form.seriesTitle,
@@ -4214,6 +4233,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
imageUrl: form.seriesImageUrl, imageUrl: form.seriesImageUrl,
season: form.seriesSeason ?? (parseInt(endSeriesNewSeason) - 1 || 1), season: form.seriesSeason ?? (parseInt(endSeriesNewSeason) - 1 || 1),
...(form.seriesStudySlug ? { studySlug: form.seriesStudySlug } : {}), ...(form.seriesStudySlug ? { studySlug: form.seriesStudySlug } : {}),
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
} }
setForm(f => ({ setForm(f => ({
...f, ...f,
@@ -4221,7 +4241,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
seriesStudySlug: '', seriesStudySlug: '',
seriesSeason: parseInt(endSeriesNewSeason) || undefined, seriesSeason: parseInt(endSeriesNewSeason) || undefined,
finishedBooks: [...(f.finishedBooks ?? []), archivedBook], finishedBooks: [...(f.finishedBooks ?? []), archivedBook],
podcastFeaturedLinks: f.secondSeries?.title?.trim() ? f.podcastFeaturedLinks : [], podcastFeaturedLinks: keptHighlights,
seriesTitle: endSeriesNewTitle.trim(), seriesTitle: endSeriesNewTitle.trim(),
seriesImageUrl: endSeriesNewImage.trim() || f.seriesImageUrl, seriesImageUrl: endSeriesNewImage.trim() || f.seriesImageUrl,
seriesLabel: 'Now Playing', seriesLabel: 'Now Playing',
@@ -4475,6 +4495,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<div key={item.id} className="study-lesson-row"> <div key={item.id} className="study-lesson-row">
<span className="study-lesson-ref" style={{ width: 70 }}>{item.episodeNumber ? `Ep. ${item.episodeNumber}` : '—'}</span> <span className="study-lesson-ref" style={{ width: 70 }}>{item.episodeNumber ? `Ep. ${item.episodeNumber}` : '—'}</span>
<span className="study-lesson-title">{item.title || <em style={{ opacity: 0.5 }}>Untitled</em>}</span> <span className="study-lesson-title">{item.title || <em style={{ opacity: 0.5 }}>Untitled</em>}</span>
<span className="study-lesson-pill draft" style={{ width: 70 }}>{highlightLaneOf(item) === 'second' ? 'Lane B' : highlightLaneOf(item) === 'general' ? 'General' : 'Lane A'}</span>
<span className={`study-lesson-pill ${item.embedUrl ? 'live' : 'draft'}`} style={{ width: 68 }}>{item.embedUrl ? 'Audio' : 'No audio'}</span> <span className={`study-lesson-pill ${item.embedUrl ? 'live' : 'draft'}`} style={{ width: 68 }}>{item.embedUrl ? 'Audio' : 'No audio'}</span>
<div className="study-lesson-actions"> <div className="study-lesson-actions">
<button type="button" className="btn-admin-ghost" onClick={() => setSelectedHighlightId(item.id)}>Edit</button> <button type="button" className="btn-admin-ghost" onClick={() => setSelectedHighlightId(item.id)}>Edit</button>
@@ -4502,6 +4523,15 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<h3>{activeHighlight.title || 'Untitled Highlight'}{activeHighlight.episodeNumber ? ` · Ep. ${activeHighlight.episodeNumber}` : ''}</h3> <h3>{activeHighlight.title || 'Untitled Highlight'}{activeHighlight.episodeNumber ? ` · Ep. ${activeHighlight.episodeNumber}` : ''}</h3>
</div> </div>
<div className="admin-array-fields"> <div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`podcast-lane-${activeHighlight.id}`}>Series</label>
<select id={`podcast-lane-${activeHighlight.id}`} value={highlightLaneOf(activeHighlight)} onChange={e => updatePodcastLink(activeHighlight.id, 'lane', e.target.value)}>
<option value="primary">Lane A — {form.seriesTitle || 'main series'}</option>
<option value="second">Lane B — {form.secondSeries?.title?.trim() || 'second series (not set)'}</option>
<option value="general">General (not tied to a series)</option>
</select>
<p className="admin-field-help">Groups this highlight under its series on the Episodes page. When a series ends, its highlights move onto that book&apos;s Finished Books page. General highlights stay.</p>
</div>
<div className="admin-field"> <div className="admin-field">
<label htmlFor={`podcast-ep-${activeHighlight.id}`}>Episode Number</label> <label htmlFor={`podcast-ep-${activeHighlight.id}`}>Episode Number</label>
<input id={`podcast-ep-${activeHighlight.id}`} type="text" placeholder="e.g. 42" value={activeHighlight.episodeNumber ?? ''} onChange={e => updatePodcastLink(activeHighlight.id, 'episodeNumber', e.target.value)} /> <input id={`podcast-ep-${activeHighlight.id}`} type="text" placeholder="e.g. 42" value={activeHighlight.episodeNumber ?? ''} onChange={e => updatePodcastLink(activeHighlight.id, 'episodeNumber', e.target.value)} />
+10
View File
@@ -13125,3 +13125,13 @@
.study-status-badge.active { background: rgba(201, 168, 76, 0.18); color: #c9a84c; border: 1px solid rgba(201, 168, 76, 0.45); } .study-status-badge.active { background: rgba(201, 168, 76, 0.18); color: #c9a84c; border: 1px solid rgba(201, 168, 76, 0.45); }
.study-status-badge.planned { background: rgba(255, 255, 255, 0.05); color: #9a9486; border: 1px solid rgba(255, 255, 255, 0.15); } .study-status-badge.planned { background: rgba(255, 255, 255, 0.05); color: #9a9486; border: 1px solid rgba(255, 255, 255, 0.15); }
.study-status-badge.complete { background: rgba(120, 170, 130, 0.16); color: #8fc79b; border: 1px solid rgba(120, 170, 130, 0.45); } .study-status-badge.complete { background: rgba(120, 170, 130, 0.16); color: #8fc79b; border: 1px solid rgba(120, 170, 130, 0.45); }
.highlight-group + .highlight-group { margin-top: 2.5rem; }
.highlight-group-title {
margin: 0 0 1rem;
font-size: 1.05rem;
letter-spacing: 0.04em;
color: #c9a84c;
border-bottom: 1px solid rgba(201, 168, 76, 0.22);
padding-bottom: 0.5rem;
}
+58 -45
View File
@@ -6,8 +6,8 @@ import QASection from './components/QASection'
import ContactForm from './components/ContactForm' import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage, StudyAccountPage, StudyCommunityPage, StudyDiscussionHubPage, StudyQuizPage, StudyResetPasswordPage, StudyVerifyEmailPage } from './studyPages' import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage, StudyAccountPage, StudyCommunityPage, StudyDiscussionHubPage, StudyQuizPage, StudyResetPasswordPage, StudyVerifyEmailPage } from './studyPages'
import { SpotifyIcon } from './icons' import { SpotifyIcon } from './icons'
import type { SiteContent, StudyProgram, Testimonial } from './content' import type { SiteContent, StudyProgram, Testimonial, PodcastFeaturedLink } from './content'
import { DEFAULTS } from './content' import { DEFAULTS, getAllHighlights } from './content'
import { usePageMeta } from './hooks/usePageMeta' import { usePageMeta } from './hooks/usePageMeta'
import { useGlobalSearch } from './hooks/useGlobalSearch' import { useGlobalSearch } from './hooks/useGlobalSearch'
import { GlobalSearch } from './components/GlobalSearch' import { GlobalSearch } from './components/GlobalSearch'
@@ -715,10 +715,49 @@ function ContactSection({ content }: { content: SiteContent }) {
) )
} }
function HighlightCard({ link }: { link: PodcastFeaturedLink }) {
const hasRichContent = !!(
link.embedUrl
|| isLikelySpotifyEpisodeUrl(link.url)
|| (link.discussionQuestions ?? []).length > 0
|| link.showNotes
|| link.showNotesSummary
|| (link.showNotesThemes ?? []).length > 0
|| (link.showNotesScripture ?? []).length > 0
|| (link.showNotesQuotes ?? []).length > 0
|| (link.showNotesResources ?? []).length > 0
)
const inner = (
<>
<div className="episode-card-meta">
{link.episodeNumber && <span className="episode-number">Ep. {link.episodeNumber}</span>}
</div>
<h3 className="episode-title">{link.title || link.url}</h3>
{link.summary && <p className="episode-desc">{link.summary}</p>}
<span className="episode-listen-cta">Listen →</span>
</>
)
if (!hasRichContent && link.url) {
return <a href={link.url} target="_blank" rel="noreferrer" className="episode-card">{inner}</a>
}
return <Link to={`/episodes/${link.id}`} className="episode-card">{inner}</Link>
}
// Highlights are grouped by the series they belong to, so each book being studied gets its own row.
function PodcastHighlightsSection({ content }: { content: SiteContent }) { function PodcastHighlightsSection({ content }: { content: SiteContent }) {
const links = [...(content.podcastFeaturedLinks ?? [])].reverse() const links = [...(content.podcastFeaturedLinks ?? [])].reverse()
if (links.length === 0) return null if (links.length === 0) return null
const laneOf = (link: PodcastFeaturedLink) => (link.lane === 'second' ? 'second' : link.lane === 'general' ? 'general' : 'primary')
const secondTitle = content.secondSeries?.title?.trim()
const groups = [
{ key: 'primary', title: content.seriesTitle, links: links.filter(link => laneOf(link) === 'primary') },
{ key: 'second', title: secondTitle, links: links.filter(link => laneOf(link) === 'second') },
{ key: 'general', title: 'More Highlights', links: links.filter(link => laneOf(link) === 'general') },
].filter(group => group.links.length > 0)
// Only label the rows when there is more than one to tell apart
const showGroupTitles = groups.length > 1
return ( return (
<section className="section-resources" aria-label="Podcast highlights"> <section className="section-resources" aria-label="Podcast highlights">
<div className="section-inner"> <div className="section-inner">
@@ -726,48 +765,14 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
<span className="ornament">✦</span> Podcast Highlights{' '} <span className="ornament">✦</span> Podcast Highlights{' '}
<span className="ornament">✦</span> <span className="ornament">✦</span>
</h2> </h2>
<div className="episode-list"> {groups.map(group => (
{links.map(link => { <div key={group.key} className="highlight-group">
const hasRichContent = !!( {showGroupTitles && <h3 className="highlight-group-title">{group.title}</h3>}
link.embedUrl <div className="episode-list">
|| isLikelySpotifyEpisodeUrl(link.url) {group.links.map(link => <HighlightCard key={link.id} link={link} />)}
|| (link.discussionQuestions ?? []).length > 0 </div>
|| link.showNotes </div>
|| link.showNotesSummary ))}
|| (link.showNotesThemes ?? []).length > 0
|| (link.showNotesScripture ?? []).length > 0
|| (link.showNotesQuotes ?? []).length > 0
|| (link.showNotesResources ?? []).length > 0
)
const dest = (!hasRichContent && link.url)
? { external: link.url }
: { internal: `/episodes/${link.id}` }
const inner = (
<>
<div className="episode-card-meta">
{link.episodeNumber && <span className="episode-number">Ep. {link.episodeNumber}</span>}
</div>
<h3 className="episode-title">{link.title || link.url}</h3>
{link.summary && <p className="episode-desc">{link.summary}</p>}
<span className="episode-listen-cta">Listen →</span>
</>
)
if ('external' in dest) {
return (
<a key={link.id} href={dest.external} target="_blank" rel="noreferrer" className="episode-card">
{inner}
</a>
)
}
return (
<Link key={link.id} to={dest.internal} className="episode-card">
{inner}
</Link>
)
})}
</div>
</div> </div>
</section> </section>
) )
@@ -1424,7 +1429,7 @@ function LandingPage({ content }: { content: SiteContent }) {
function EpisodeDetailPage({ content }: { content: SiteContent }) { function EpisodeDetailPage({ content }: { content: SiteContent }) {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id) const episode = getAllHighlights(content).find(e => e.id === id)
const [resolvedEmbedUrl, setResolvedEmbedUrl] = useState('') const [resolvedEmbedUrl, setResolvedEmbedUrl] = useState('')
useEffect(() => { useEffect(() => {
@@ -1751,6 +1756,14 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
)} )}
</div> </div>
</div> </div>
{(book.highlights ?? []).length > 0 && (
<>
<h2 className="finished-series-playlist-heading">Episode Highlights</h2>
<div className="episode-list">
{[...(book.highlights ?? [])].reverse().map(link => <HighlightCard key={link.id} link={link} />)}
</div>
</>
)}
<h2 id="playlist" className="finished-series-playlist-heading">Episode Playlist</h2> <h2 id="playlist" className="finished-series-playlist-heading">Episode Playlist</h2>
{loading ? ( {loading ? (
<p className="finished-books-empty">Loading episodes…</p> <p className="finished-books-empty">Loading episodes…</p>
+13
View File
@@ -68,6 +68,9 @@ export interface RedirectRule {
export interface PodcastFeaturedLink { export interface PodcastFeaturedLink {
id: string id: string
// Which series this highlight belongs to: Lane A (primary), Lane B (second), or general.
// Missing = primary (older highlights were all about the one running series).
lane?: 'primary' | 'second' | 'general'
title: string title: string
episodeNumber?: string episodeNumber?: string
summary: string summary: string
@@ -110,6 +113,8 @@ export interface FinishedBook {
season: number season: number
// The study that goes with this book, so the card can offer both "Listen" and "Study" // The study that goes with this book, so the card can offer both "Listen" and "Study"
studySlug?: string studySlug?: string
// Episode highlights that were featured while this series was playing, kept for its page
highlights?: PodcastFeaturedLink[]
downloadUrl?: string downloadUrl?: string
downloadQrUrl?: string downloadQrUrl?: string
} }
@@ -666,3 +671,11 @@ export const DEFAULTS: SiteContent = {
], ],
}, },
} }
// Highlights currently featured plus those archived onto finished books (so their detail pages keep working).
export function getAllHighlights(content: Pick<SiteContent, 'podcastFeaturedLinks' | 'finishedBooks'>): PodcastFeaturedLink[] {
return [
...(content.podcastFeaturedLinks ?? []),
...(content.finishedBooks ?? []).flatMap(book => book.highlights ?? []),
]
}
+3 -2
View File
@@ -1,6 +1,7 @@
import { useMemo, useState, useEffect, useRef } from 'react' import { useMemo, useState, useEffect, useRef } from 'react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import type { SiteContent } from '../content' import type { SiteContent } from '../content'
import { getAllHighlights } from '../content'
export type SearchResultType = 'episode' | 'question' | 'resource' | 'series' | 'newsletter' export type SearchResultType = 'episode' | 'question' | 'resource' | 'series' | 'newsletter'
@@ -58,7 +59,7 @@ export function useGlobalSearch(content: SiteContent) {
.then((data: { episodes?: Array<{ title: string; link: string; description?: string; episode?: string }> }) => { .then((data: { episodes?: Array<{ title: string; link: string; description?: string; episode?: string }> }) => {
const eps: SearchResult[] = (data.episodes ?? []).map(ep => { const eps: SearchResult[] = (data.episodes ?? []).map(ep => {
// Check if there's a matching podcastFeaturedLink highlight page for this episode number // Check if there's a matching podcastFeaturedLink highlight page for this episode number
const highlight = content.podcastFeaturedLinks?.find( const highlight = getAllHighlights(content).find(
f => f.episodeNumber && ep.episode && String(f.episodeNumber) === String(ep.episode) f => f.episodeNumber && ep.episode && String(f.episodeNumber) === String(ep.episode)
) )
const hasRichPage = highlight && !!( const hasRichPage = highlight && !!(
@@ -76,7 +77,7 @@ export function useGlobalSearch(content: SiteContent) {
setEpisodeItems(eps) setEpisodeItems(eps)
}) })
.catch(() => {}) .catch(() => {})
}, [content.podcastFeaturedLinks]) }, [content])
// Fetch nuggets episodes once on mount // Fetch nuggets episodes once on mount
useEffect(() => { useEffect(() => {