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
Skip to content
Luke at the Roost
{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'{_esc(published)} ')
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' '
f"← {_esc(prev_ep.title)} "
)
if next_ep is not None:
links.append(
f' '
f"{_esc(next_ep.title)} → "
)
if not links:
return ""
return ' \n' + "\n".join(links) + "\n \n"