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

+58 -45
View File
@@ -6,8 +6,8 @@ import QASection from './components/QASection'
import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage, StudyAccountPage, StudyCommunityPage, StudyDiscussionHubPage, StudyQuizPage, StudyResetPasswordPage, StudyVerifyEmailPage } from './studyPages'
import { SpotifyIcon } from './icons'
import type { SiteContent, StudyProgram, Testimonial } from './content'
import { DEFAULTS } from './content'
import type { SiteContent, StudyProgram, Testimonial, PodcastFeaturedLink } from './content'
import { DEFAULTS, getAllHighlights } from './content'
import { usePageMeta } from './hooks/usePageMeta'
import { useGlobalSearch } from './hooks/useGlobalSearch'
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 }) {
const links = [...(content.podcastFeaturedLinks ?? [])].reverse()
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 (
<section className="section-resources" aria-label="Podcast highlights">
<div className="section-inner">
@@ -726,48 +765,14 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
<span className="ornament">✦</span> Podcast Highlights{' '}
<span className="ornament">✦</span>
</h2>
<div className="episode-list">
{links.map(link => {
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 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>
{groups.map(group => (
<div key={group.key} className="highlight-group">
{showGroupTitles && <h3 className="highlight-group-title">{group.title}</h3>}
<div className="episode-list">
{group.links.map(link => <HighlightCard key={link.id} link={link} />)}
</div>
</div>
))}
</div>
</section>
)
@@ -1424,7 +1429,7 @@ function LandingPage({ content }: { content: SiteContent }) {
function EpisodeDetailPage({ content }: { content: SiteContent }) {
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('')
useEffect(() => {
@@ -1751,6 +1756,14 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
)}
</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>
{loading ? (
<p className="finished-books-empty">Loading episodes…</p>