import html import json from datetime import datetime SITE_URL = "https://lukeattheroost.com" COVER_IMAGE = "https://cdn.lukeattheroost.com/media/podcasts/LukeAtTheRoost/cover_feed.png?v=3" FEED_URL = "https://podcast.macneilmediagroup.com/@LukeAtTheRoost/feed.xml" PAGE = """ {title} — Luke at the Roost

Transcript

{transcript}
{episode_nav}
""" def render_episode_page(episode, turns, prev_ep=None, next_ep=None) -> str: """Render a complete static episode page, transcript and schema included.""" page_url = _page_url(episode.slug) return PAGE.format( title=_esc(episode.title), meta_description=_esc(_truncate(episode.description, 160)), description=_esc(episode.description), page_url=_esc(page_url), cover=_esc(COVER_IMAGE), feed=_esc(FEED_URL), audio_url=_esc(episode.audio_url), meta_line=_meta_line(episode), schema=_schema_block(episode, page_url), transcript=_transcript_html(turns), episode_nav=_episode_nav(prev_ep, next_ep), ) def _page_url(slug: str) -> str: return f"{SITE_URL}/episode/{slug}/" def _esc(value) -> str: return html.escape(str(value or ""), quote=True) def _truncate(text: str, limit: int) -> str: text = (text or "").strip() if len(text) <= limit: return text return text[:limit].rsplit(" ", 1)[0].rstrip(",.;:—- ") + "…" def _schema_block(episode, page_url: str) -> str: ep_schema = { "@context": "https://schema.org", "@type": "PodcastEpisode", "url": page_url, "name": episode.title, "description": episode.description, "associatedMedia": {"@type": "MediaObject", "contentUrl": episode.audio_url}, "partOfSeries": { "@type": "PodcastSeries", "name": "Luke at the Roost", "url": SITE_URL, }, "contentLocation": { "@type": "Place", "name": "Big Bend, West Texas", "address": { "@type": "PostalAddress", "addressLocality": "Alpine", "addressRegion": "TX", "addressCountry": "US", }, }, } if episode.published_iso: ep_schema["datePublished"] = episode.published_iso if episode.duration_seconds: ep_schema["timeRequired"] = f"PT{episode.duration_seconds}S" if episode.number is not None: ep_schema["episodeNumber"] = episode.number breadcrumbs = { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ {"@type": "ListItem", "position": 1, "name": "Home", "item": SITE_URL}, {"@type": "ListItem", "position": 2, "name": episode.title, "item": page_url}, ], } dumped = json.dumps([ep_schema, breadcrumbs], indent=2, ensure_ascii=False) return dumped.replace("<", "\\u003c") def _meta_line(episode) -> str: parts = [] published = _format_date(episode.published_iso) if published: parts.append(f'') duration = _format_duration(episode.duration_seconds) if duration: parts.append(_esc(duration)) return " · ".join(parts) def _format_date(published_iso: str) -> str: try: dt = datetime.fromisoformat(published_iso) return f"{dt.strftime('%B')} {dt.day}, {dt.year}" except (TypeError, ValueError): return "" def _format_duration(seconds) -> str: if not seconds: return "" hours, minutes = divmod(int(seconds) // 60, 60) if hours: return f"{hours} hr {minutes} min" return f"{minutes} min" def _transcript_html(turns) -> str: if not turns: return '

Transcript not yet available for this episode.

' rows = [ f'
' f'{_esc(speaker)}' f"

{_esc(text)}

" for speaker, text in turns ] return "\n".join(rows) def _episode_nav(prev_ep, next_ep) -> str: links = [] if prev_ep is not None: links.append( f' " ) if next_ep is not None: links.append( f' " ) if not links: return "" return ' \n"