Files
claudetools/projects/radio-show/website/src/pages/episodes/[...slug].astro
Mike Swanson ee89727662 Radio show website: Full Astro build with 194 episodes imported
Complete website for The Computer Guru Show (radio.azcomputerguru.com):
- Astro 6.0.4 static site with React islands
- 194 episodes imported from gurushow.com RSS feed
- Dark/light mode HSL design system
- Persistent audio player with session persistence
- Episode archive with search and season filtering
- Home page with animated hero, stats, latest episodes
- All pages: About, Subscribe, Community, Live, Contact, Blog, 404
- Podcast RSS feed with iTunes namespace
- Session log updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 20:44:42 -07:00

459 lines
12 KiB
Plaintext

---
import BaseLayout from '../../layouts/BaseLayout.astro';
import EpisodeCard from '../../components/episodes/EpisodeCard.astro';
import { getCollection, render } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const episodes = await getCollection('episodes');
return episodes.map((episode) => ({
params: { slug: episode.id },
props: { episode },
}));
}
interface Props {
episode: CollectionEntry<'episodes'>;
}
const { episode } = Astro.props;
const { title, season, episode: episodeNum, pubDate, duration, audioUrl, tags, chapters, originalUrl } = episode.data;
const { Content } = await render(episode);
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
const seasonLabel = season === 0 ? 'Special' : `Season ${season}`;
const episodeLabel = season === 0 ? 'Special' : `S${season} E${String(episodeNum).padStart(2, '0')}`;
const allEpisodes = await getCollection('episodes');
const relatedEpisodes = allEpisodes
.filter((ep) => ep.data.season === season && ep.id !== episode.id)
.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime())
.slice(0, 4);
---
<BaseLayout title={title} description={`${episodeLabel} of The Computer Guru Show`}>
<article class="episode-detail">
<div class="container container--narrow">
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a href="/episodes">Episodes</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">/</li>
<li class="breadcrumb__item">
<a href={`/episodes?season=${season}`}>{seasonLabel}</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">/</li>
<li class="breadcrumb__item breadcrumb__item--current" aria-current="page">
{title}
</li>
</ol>
</nav>
<header class="episode-detail__header">
<span class="badge">{episodeLabel}</span>
<h1 class="episode-detail__title">{title}</h1>
<div class="episode-detail__meta">
<time datetime={pubDate.toISOString()} class="episode-detail__date">
{formattedDate}
</time>
<span class="episode-detail__divider" aria-hidden="true">|</span>
<span class="episode-detail__duration">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{duration}
</span>
</div>
</header>
<section class="episode-detail__player">
<audio
controls
preload="metadata"
class="episode-detail__audio"
src={audioUrl}
>
Your browser does not support the audio element.
</audio>
<button
class="btn btn--ghost episode-detail__play-persistent"
data-audio-url={audioUrl}
data-episode-title={title}
data-season={String(season)}
data-episode-num={String(episodeNum)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="5 3 19 12 5 21 5 3" fill="currentColor"></polygon>
</svg>
Play in Persistent Player
</button>
</section>
{chapters && chapters.length > 0 && (
<section class="episode-detail__chapters">
<h2 class="episode-detail__section-title">Chapters</h2>
<ol class="chapters-list">
{chapters.map((chapter) => (
<li class="chapters-list__item">
<span class="chapters-list__time">{chapter.time}</span>
<span class="chapters-list__title">{chapter.title}</span>
</li>
))}
</ol>
</section>
)}
<section class="episode-detail__content">
<h2 class="episode-detail__section-title">Show Notes</h2>
<div class="prose">
<Content />
</div>
</section>
{tags.length > 0 && (
<section class="episode-detail__tags">
<h2 class="episode-detail__section-title">Tags</h2>
<div class="tag-list">
{tags.map((tag: string) => (
<span class="badge">{tag}</span>
))}
</div>
</section>
)}
<section class="episode-detail__platforms">
<h2 class="episode-detail__section-title">Listen On</h2>
<div class="platform-links">
<a
href={audioUrl}
class="btn btn--ghost"
download
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download MP3
</a>
{originalUrl && (
<a
href={originalUrl}
class="btn btn--ghost"
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
Original Post
</a>
)}
<a
href="/feed.xml"
class="btn btn--ghost"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11a9 9 0 0 1 9 9"></path>
<path d="M4 4a16 16 0 0 1 16 16"></path>
<circle cx="5" cy="19" r="1"></circle>
</svg>
RSS Feed
</a>
</div>
</section>
</div>
{relatedEpisodes.length > 0 && (
<section class="episode-detail__related section">
<div class="container">
<h2 class="section__title">More from {seasonLabel}</h2>
<div class="grid grid--3">
{relatedEpisodes.map((ep) => (
<EpisodeCard episode={ep} />
))}
</div>
</div>
</section>
)}
</article>
</BaseLayout>
<style>
.breadcrumb {
margin-bottom: var(--space-8);
padding-top: var(--space-6);
}
.breadcrumb__list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
list-style: none;
padding: 0;
margin: 0;
}
.breadcrumb__item a {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.breadcrumb__item a:hover {
color: var(--color-accent);
}
.breadcrumb__item--current {
font-size: var(--text-sm);
color: var(--color-text-secondary);
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.breadcrumb__separator {
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.episode-detail__header {
margin-bottom: var(--space-8);
}
.episode-detail__header .badge {
margin-bottom: var(--space-3);
}
.episode-detail__title {
font-size: var(--text-3xl);
margin-top: var(--space-3);
margin-bottom: var(--space-4);
line-height: 1.15;
}
.episode-detail__meta {
display: flex;
align-items: center;
gap: var(--space-3);
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.episode-detail__duration {
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.episode-detail__divider {
color: var(--color-border);
}
.episode-detail__player {
margin-bottom: var(--space-8);
padding: var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.episode-detail__audio {
width: 100%;
height: 48px;
}
.episode-detail__play-persistent {
margin-top: var(--space-3);
font-size: var(--text-sm);
}
.episode-detail__audio::-webkit-media-controls-panel {
background: var(--color-bg-tertiary);
}
.episode-detail__section-title {
font-size: var(--text-xl);
margin-bottom: var(--space-4);
}
.episode-detail__chapters {
margin-bottom: var(--space-8);
}
.chapters-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
overflow: hidden;
}
.chapters-list__item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
transition: background var(--transition-fast);
}
.chapters-list__item:last-child {
border-bottom: none;
}
.chapters-list__item:hover {
background: var(--color-bg-tertiary);
}
.chapters-list__time {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-accent);
min-width: 60px;
flex-shrink: 0;
}
.chapters-list__title {
font-size: var(--text-sm);
color: var(--color-text-primary);
}
.episode-detail__content {
margin-bottom: var(--space-8);
}
.prose {
color: var(--color-text-secondary);
line-height: 1.8;
font-size: var(--text-base);
}
.prose :global(p) {
margin-bottom: var(--space-4);
}
.prose :global(a) {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.prose :global(a:hover) {
color: var(--color-accent-hover);
}
.prose :global(ul),
.prose :global(ol) {
margin-bottom: var(--space-4);
padding-left: var(--space-6);
}
.prose :global(li) {
margin-bottom: var(--space-2);
}
.prose :global(strong) {
color: var(--color-text-primary);
font-weight: 600;
}
.prose :global(h2),
.prose :global(h3) {
margin-top: var(--space-8);
margin-bottom: var(--space-3);
}
.episode-detail__tags {
margin-bottom: var(--space-8);
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.episode-detail__platforms {
margin-bottom: var(--space-8);
padding: var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.platform-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
}
.episode-detail__related {
border-top: 1px solid var(--color-border);
}
@media (max-width: 640px) {
.episode-detail__title {
font-size: var(--text-2xl);
}
.episode-detail__meta {
flex-direction: column;
align-items: flex-start;
gap: var(--space-1);
}
.episode-detail__divider {
display: none;
}
.platform-links {
flex-direction: column;
}
.platform-links .btn {
width: 100%;
justify-content: center;
}
}
</style>
<script>
document.addEventListener('astro:page-load', () => {
const btn = document.querySelector<HTMLButtonElement>('.episode-detail__play-persistent');
if (btn) {
btn.addEventListener('click', (e) => {
e.preventDefault();
const audioUrl = btn.dataset.audioUrl;
const title = btn.dataset.episodeTitle;
const season = parseInt(btn.dataset.season ?? '0', 10);
const episode = parseInt(btn.dataset.episodeNum ?? '0', 10);
if (audioUrl) {
document.dispatchEvent(
new CustomEvent('play-episode', {
detail: { audioUrl, title, season, episode },
})
);
}
});
}
});
</script>