From fa5ab9aad2432956234a60c707c1da9f48a509dd Mon Sep 17 00:00:00 2001 From: tcpsyn Date: Fri, 14 Aug 2026 04:10:03 -0500 Subject: [PATCH] Point internal links at clean episode URLs and retire the JS page --- publish_episode.py | 35 +------ tests/test_no_legacy_episode_urls.py | 35 +++++++ website/_redirects | 2 +- website/episode.html | 127 ------------------------ website/js/app.js | 8 +- website/js/episode.js | 142 --------------------------- website/llms.txt | 14 +-- 7 files changed, 50 insertions(+), 313 deletions(-) create mode 100644 tests/test_no_legacy_episode_urls.py delete mode 100644 website/episode.html delete mode 100644 website/js/episode.js diff --git a/publish_episode.py b/publish_episode.py index f053adf..15fd1f5 100755 --- a/publish_episode.py +++ b/publish_episode.py @@ -974,34 +974,6 @@ def sync_episode_media_to_bunny(episode_id: int, already_uploaded: set): Path(tmp_path).unlink(missing_ok=True) -def add_episode_to_sitemap(slug: str): - """Add episode transcript page to sitemap.xml.""" - sitemap_path = Path(__file__).parent / "website" / "sitemap.xml" - if not sitemap_path.exists(): - return - - url = f"https://lukeattheroost.com/episode.html?slug={slug}" - content = sitemap_path.read_text() - - if url in content: - print(f" Episode already in sitemap") - return - - today = datetime.now().strftime("%Y-%m-%d") - new_entry = f""" - {url} - {today} - never - 0.7 - -""" - - content = content.replace("", new_entry) - sitemap_path.write_text(content) - print(f" Added episode to sitemap.xml") - - - def generate_social_image(episode_number: int, description: str, output_path: str) -> str: """Generate a social media image with cover art, episode number, and description.""" from PIL import Image, ImageDraw, ImageFont @@ -1266,7 +1238,7 @@ def post_to_social(metadata: dict, episode_slug: str, image_path: str = None, if media and media.get("id"): image_ids = [{"id": media["id"], "path": media.get("path", "")}] - episode_url = f"https://lukeattheroost.com/episode.html?slug={episode_slug}" + episode_url = f"https://lukeattheroost.com/episode/{episode_slug}/" yt_url = f"https://youtube.com/watch?v={yt_video_id}" if yt_video_id else None now = datetime.now(timezone.utc) @@ -1468,7 +1440,7 @@ def upload_to_youtube(audio_path: str, metadata: dict, chapters: list, ts = f"{h}:{m:02d}:{s:02d}" if h > 0 else f"{m}:{s:02d}" chapter_lines.append(f"{ts} {ch['title']}") - episode_url = f"https://lukeattheroost.com/episode.html?slug={episode_slug}" + episode_url = f"https://lukeattheroost.com/episode/{episode_slug}/" description = ( f"{metadata['description']}\n\n" + "\n".join(chapter_lines) + "\n\n" @@ -1845,8 +1817,7 @@ def main(): shutil.copy2(str(transcript_path), str(website_transcript_path)) print(f" Transcript copied to website/transcripts/") - # Add to sitemap - add_episode_to_sitemap(episode["slug"]) + # Sitemap is regenerated wholesale by generate_episode_pages.py --sitemap # Sync any remaining episode media to BunnyCDN (cover art, etc.) print(" Syncing remaining episode media to CDN...") diff --git a/tests/test_no_legacy_episode_urls.py b/tests/test_no_legacy_episode_urls.py new file mode 100644 index 0000000..c06a8c3 --- /dev/null +++ b/tests/test_no_legacy_episode_urls.py @@ -0,0 +1,35 @@ +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +LEGACY = "episode.html?slug=" + + +def _files(): + for pattern in ("website/*.html", "website/js/*.js", "website/llms.txt", "*.py"): + yield from ROOT.glob(pattern) + + +def test_no_source_file_builds_a_legacy_episode_url(): + offenders = [] + for f in _files(): + if f.name == "_worker.js": + continue # the worker's reference is the redirect itself + if LEGACY in f.read_text(errors="replace"): + offenders.append(str(f.relative_to(ROOT))) + assert not offenders, f"legacy episode URLs still present: {offenders}" + + +def test_client_rendered_episode_page_is_gone(): + assert not (ROOT / "website" / "episode.html").exists() + assert not (ROOT / "website" / "js" / "episode.js").exists() + + +def test_worker_still_redirects_legacy_urls(): + worker = (ROOT / "website" / "_worker.js").read_text() + assert "/episode.html" in worker and "301" in worker + + +def test_redirects_file_has_no_dead_episode_target(): + redirects = (ROOT / "website" / "_redirects").read_text() + assert "/episode 302" not in redirects diff --git a/website/_redirects b/website/_redirects index 3ed17a5..9b9d6f1 100644 --- a/website/_redirects +++ b/website/_redirects @@ -1 +1 @@ -/episodes.html /episode 302 +/episodes.html / 302 diff --git a/website/episode.html b/website/episode.html deleted file mode 100644 index f7637e4..0000000 --- a/website/episode.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - Episode — Luke at the Roost - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
-

Loading...

-

-
- -
-
-
- - -
-

Full Transcript

-
-
Loading transcript...
-
-
- -
- - - - - - -
-
- -
-
-
-
-
-
- 0:00 / 0:00 -
-
-
-
- - - - - - - - diff --git a/website/js/app.js b/website/js/app.js index ffdff53..23f1256 100644 --- a/website/js/app.js +++ b/website/js/app.js @@ -107,7 +107,7 @@ function createFeaturedCard(ep) { - ${epSlug ? `Read Transcript` : ''} + ${epSlug ? `Read Transcript` : ''} `; @@ -117,7 +117,7 @@ function createFeaturedCard(ep) { const shareBtn = card.querySelector('.episode-share-btn'); const shareUrl = epSlug - ? `${window.location.origin}/episode.html?slug=${encodeURIComponent(epSlug)}` + ? `${window.location.origin}/episode/${encodeURIComponent(epSlug)}/` : window.location.origin; shareBtn.addEventListener('click', () => shareContent(ep.title, shareUrl, shareBtn)); @@ -207,7 +207,7 @@ function createEpisodeCard(ep) {
${metaParts}
${escapeAttr(ep.title)}
${truncate(ep.description, 150)}
- ${epSlug ? `Read Transcript` : ''} + ${epSlug ? `Read Transcript` : ''} `; @@ -217,7 +217,7 @@ function createEpisodeCard(ep) { const shareBtn = card.querySelector('.episode-share-btn'); const shareUrl = epSlug - ? `${window.location.origin}/episode.html?slug=${encodeURIComponent(epSlug)}` + ? `${window.location.origin}/episode/${encodeURIComponent(epSlug)}/` : window.location.origin; shareBtn.addEventListener('click', () => shareContent(ep.title, shareUrl, shareBtn)); diff --git a/website/js/episode.js b/website/js/episode.js deleted file mode 100644 index aab70f8..0000000 --- a/website/js/episode.js +++ /dev/null @@ -1,142 +0,0 @@ -const FEED_URL = '/feed'; - -function formatDate(dateStr) { - return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); -} - -function parseDuration(raw) { - if (!raw) return ''; - if (raw.includes(':')) { - const parts = raw.split(':').map(Number); - let t = 0; - if (parts.length === 3) t = parts[0]*3600 + parts[1]*60 + parts[2]; - else if (parts.length === 2) t = parts[0]*60 + parts[1]; - return `${Math.round(t/60)} min`; - } - const sec = parseInt(raw, 10); - return isNaN(sec) ? '' : `${Math.round(sec/60)} min`; -} - -function stripHtml(html) { - const div = document.createElement('div'); - div.innerHTML = html || ''; - return div.textContent || ''; -} - -function escapeHtml(str) { - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} - -// Get slug from URL -const params = new URLSearchParams(window.location.search); -const slug = params.get('slug'); - -if (!slug) { - document.getElementById('ep-title').textContent = 'Episode not found'; - document.getElementById('transcript-body').innerHTML = '

No episode specified. Go back to episodes.

'; -} else { - loadEpisode(slug); -} - -async function loadEpisode(slug) { - try { - const res = await fetch(FEED_URL); - const xml = await res.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, 'text/xml'); - const items = doc.querySelectorAll('item'); - - let episode = null; - for (const item of items) { - const link = item.querySelector('link')?.textContent || ''; - const itemSlug = link.split('/episodes/').pop()?.replace(/\/$/, ''); - if (itemSlug === slug) { - episode = { - title: item.querySelector('title')?.textContent || 'Untitled', - description: item.querySelector('description')?.textContent || '', - audioUrl: item.querySelector('enclosure')?.getAttribute('url') || '', - pubDate: item.querySelector('pubDate')?.textContent || '', - duration: item.getElementsByTagNameNS('http://www.itunes.com/dtds/podcast-1.0.dtd', 'duration')[0]?.textContent || '', - episodeNum: item.getElementsByTagNameNS('http://www.itunes.com/dtds/podcast-1.0.dtd', 'episode')[0]?.textContent || '', - }; - break; - } - } - - if (!episode) { - document.getElementById('ep-title').textContent = 'Episode not found'; - document.getElementById('transcript-body').innerHTML = '

Could not find this episode. Go back to episodes.

'; - return; - } - - // Populate header - const metaParts = [ - episode.episodeNum ? `Episode ${episode.episodeNum}` : '', - episode.pubDate ? formatDate(episode.pubDate) : '', - parseDuration(episode.duration), - ].filter(Boolean).join(' \u00b7 '); - - document.getElementById('ep-meta').textContent = metaParts; - document.getElementById('ep-title').textContent = episode.title; - document.getElementById('ep-desc').textContent = stripHtml(episode.description || ''); - - // Update page meta - document.title = `${episode.title} — Luke at the Roost`; - document.getElementById('page-description')?.setAttribute('content', `Full transcript of ${episode.title} from Luke at the Roost.`); - document.getElementById('og-title')?.setAttribute('content', episode.title); - document.getElementById('og-description')?.setAttribute('content', stripHtml(episode.description).slice(0, 200)); - const canonicalUrl = `https://lukeattheroost.com/episode.html?slug=${slug}`; - document.getElementById('page-canonical')?.setAttribute('href', canonicalUrl); - document.getElementById('og-url')?.setAttribute('content', canonicalUrl); - document.getElementById('tw-title')?.setAttribute('content', episode.title); - document.getElementById('tw-description')?.setAttribute('content', stripHtml(episode.description).slice(0, 200)); - - // Update JSON-LD structured data - const jsonLd = document.getElementById('episode-jsonld'); - if (jsonLd) { - const ld = JSON.parse(jsonLd.textContent); - ld.name = episode.title; - ld.url = canonicalUrl; - ld.description = stripHtml(episode.description).slice(0, 300); - if (episode.pubDate) ld.datePublished = new Date(episode.pubDate).toISOString().split('T')[0]; - if (episode.episodeNum) ld.episodeNumber = parseInt(episode.episodeNum, 10); - if (episode.audioUrl) { - ld.associatedMedia = { - "@type": "MediaObject", - "contentUrl": episode.audioUrl - }; - } - jsonLd.textContent = JSON.stringify(ld); - } - - // Play button - if (episode.audioUrl) { - const playBtn = document.getElementById('ep-play-btn'); - playBtn.style.display = 'inline-flex'; - playBtn.addEventListener('click', () => { - audio.src = episode.audioUrl; - audio.play(); - playerTitle.textContent = episode.title; - stickyPlayer.classList.add('active'); - }); - } - } catch (e) { - document.getElementById('ep-title').textContent = 'Error loading episode'; - } - - // Fetch transcript - try { - const txRes = await fetch(`/transcripts/${slug}.txt`); - if (!txRes.ok) throw new Error('Not found'); - const text = await txRes.text(); - const paragraphs = text.split(/\n\n+/).filter(Boolean); - const html = paragraphs.map(p => { - const escaped = escapeHtml(p); - const labeled = escaped.replace(/^([A-Z][A-Z\s'\-]+?):\s*/, '$1: '); - return `

${labeled.replace(/\n/g, '
')}

`; - }).join(''); - document.getElementById('transcript-body').innerHTML = html; - } catch (e) { - document.getElementById('transcript-body').innerHTML = '

Transcript not yet available for this episode.

'; - } -} diff --git a/website/llms.txt b/website/llms.txt index 007076f..4e15437 100644 --- a/website/llms.txt +++ b/website/llms.txt @@ -48,7 +48,7 @@ The show is entirely custom-built: callers are generated in real-time using larg - **How It Works**: https://lukeattheroost.com/how-it-works — Technical deep dive into the AI caller generation, recording, and production pipeline - **Clips**: https://lukeattheroost.com/clips — Best moments from the show as video clips - **Stats**: https://lukeattheroost.com/stats — Download numbers, reviews, YouTube metrics -- **Episode transcripts**: https://lukeattheroost.com/episode.html?slug=EPISODE-SLUG — Full text transcripts of every episode +- **Episode transcripts**: https://lukeattheroost.com/episode/EPISODE-SLUG/ — Full text transcripts of every episode ## Community @@ -121,16 +121,16 @@ Each episode (~30-60 minutes) follows a loose late-night radio format: ## Recent Episodes Episodes are published daily. Each has a full transcript available at: -https://lukeattheroost.com/episode.html?slug=EPISODE-SLUG +https://lukeattheroost.com/episode/EPISODE-SLUG/ Episode transcript URLs follow the pattern: episode-N-title-slug Recent episodes include: -- Episode 37: "Secrets, Lies, and Coffee Runs" — https://lukeattheroost.com/episode.html?slug=episode-37-secrets-lies-and-coffee-runs -- Episode 36: "Late Night Confessions and Unexpected Moments" — https://lukeattheroost.com/episode.html?slug=episode-36-late-night-confessions-and-unexpected-moments -- Episode 35: "Midnight Confessions and Unexpected Revelations" — https://lukeattheroost.com/episode.html?slug=episode-35-midnight-confessions-and-unexpected-revelations -- Episode 34: "Hidden Rooms, Potlucks, and Mysterious Notes" — https://lukeattheroost.com/episode.html?slug=episode-34-hidden-rooms-potlucks-and-mysterious-notes -- Episode 33: "Late Night Confessions and Cosmic Comedies" — https://lukeattheroost.com/episode.html?slug=episode-33-late-night-confessions-and-cosmic-comedies +- Episode 37: "Secrets, Lies, and Coffee Runs" — https://lukeattheroost.com/episode/episode-37-secrets-lies-and-coffee-runs/ +- Episode 36: "Late Night Confessions and Unexpected Moments" — https://lukeattheroost.com/episode/episode-36-late-night-confessions-and-unexpected-moments/ +- Episode 35: "Midnight Confessions and Unexpected Revelations" — https://lukeattheroost.com/episode/episode-35-midnight-confessions-and-unexpected-revelations/ +- Episode 34: "Hidden Rooms, Potlucks, and Mysterious Notes" — https://lukeattheroost.com/episode/episode-34-hidden-rooms-potlucks-and-mysterious-notes/ +- Episode 33: "Late Night Confessions and Cosmic Comedies" — https://lukeattheroost.com/episode/episode-33-late-night-confessions-and-cosmic-comedies/ ## Clip Highlights