feat: v1.2.0-rc.2 - Media display fixes and feed enhancements
## Added - Feed Media Enhancement with Media RSS namespace support - RSS enclosure, media:content, media:thumbnail elements - JSON Feed image field for first image - ADR-059: Full feed media standardization roadmap ## Fixed - Media display on homepage (was only showing on note pages) - Responsive image sizing with CSS constraints - Caption display (now alt text only, not visible) - Logging correlation ID crash in non-request contexts ## Documentation - Feed media design documents and implementation reports - Media display fixes design and validation reports - Updated ROADMAP with v1.3.0/v1.4.0 media plans 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -142,8 +142,23 @@ def generate_rss(
|
||||
# feedgen automatically wraps content in CDATA for RSS
|
||||
fe.description(html_content)
|
||||
|
||||
# Add RSS enclosure element (first image only, per RSS 2.0 spec)
|
||||
if hasattr(note, 'media') and note.media:
|
||||
first_media = note.media[0]
|
||||
media_url = f"{site_url}/media/{first_media['path']}"
|
||||
fe.enclosure(
|
||||
url=media_url,
|
||||
length=str(first_media.get('size', 0)),
|
||||
type=first_media.get('mime_type', 'image/jpeg')
|
||||
)
|
||||
|
||||
# Generate RSS 2.0 XML (pretty-printed)
|
||||
feed_xml = fg.rss_str(pretty=True).decode("utf-8")
|
||||
feed_xml_bytes = fg.rss_str(pretty=True)
|
||||
feed_xml = feed_xml_bytes.decode("utf-8")
|
||||
|
||||
# Add Media RSS elements manually (feedgen's media extension has issues)
|
||||
# We need to inject media:content and media:thumbnail elements
|
||||
feed_xml = _inject_media_rss_elements(feed_xml, site_url, notes[:limit])
|
||||
|
||||
# Track feed generation metrics
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
@@ -215,9 +230,9 @@ def generate_rss_streaming(
|
||||
now = datetime.now(timezone.utc)
|
||||
last_build = format_rfc822_date(now)
|
||||
|
||||
# Yield XML declaration and opening RSS tag
|
||||
# Yield XML declaration and opening RSS tag with Media RSS namespace
|
||||
yield '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
yield '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n'
|
||||
yield '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">\n'
|
||||
yield " <channel>\n"
|
||||
|
||||
# Yield channel metadata
|
||||
@@ -245,16 +260,54 @@ def generate_rss_streaming(
|
||||
pubdate = pubdate.replace(tzinfo=timezone.utc)
|
||||
pub_date_str = format_rfc822_date(pubdate)
|
||||
|
||||
# Get HTML content
|
||||
html_content = clean_html_for_rss(note.html)
|
||||
# Build HTML content with media (per Q24 and ADR-057)
|
||||
html_content = ""
|
||||
|
||||
# Yield complete item as a single chunk
|
||||
# Add media at top if present
|
||||
if hasattr(note, 'media') and note.media:
|
||||
html_content += '<div class="media">'
|
||||
for item in note.media:
|
||||
media_url = f"{site_url}/media/{item['path']}"
|
||||
caption = item.get('caption', '')
|
||||
html_content += f'<img src="{media_url}" alt="{caption}" />'
|
||||
html_content += '</div>'
|
||||
|
||||
# Add text content below media
|
||||
html_content += clean_html_for_rss(note.html)
|
||||
|
||||
# Build item XML
|
||||
item_xml = f""" <item>
|
||||
<title>{_escape_xml(title)}</title>
|
||||
<link>{_escape_xml(permalink)}</link>
|
||||
<guid isPermaLink="true">{_escape_xml(permalink)}</guid>
|
||||
<pubDate>{pub_date_str}</pubDate>
|
||||
<description><![CDATA[{html_content}]]></description>
|
||||
<pubDate>{pub_date_str}</pubDate>"""
|
||||
|
||||
# Add enclosure element (first image only, per RSS 2.0 spec)
|
||||
if hasattr(note, 'media') and note.media:
|
||||
first_media = note.media[0]
|
||||
media_url = f"{site_url}/media/{first_media['path']}"
|
||||
item_xml += f"""
|
||||
<enclosure url="{_escape_xml(media_url)}" length="{first_media.get('size', 0)}" type="{first_media.get('mime_type', 'image/jpeg')}"/>"""
|
||||
|
||||
# Add description with HTML content
|
||||
item_xml += f"""
|
||||
<description><![CDATA[{html_content}]]></description>"""
|
||||
|
||||
# Add media:content elements (all images)
|
||||
if hasattr(note, 'media') and note.media:
|
||||
for media_item in note.media:
|
||||
media_url = f"{site_url}/media/{media_item['path']}"
|
||||
item_xml += f"""
|
||||
<media:content url="{_escape_xml(media_url)}" type="{media_item.get('mime_type', 'image/jpeg')}" medium="image" fileSize="{media_item.get('size', 0)}"/>"""
|
||||
|
||||
# Add media:thumbnail (first image only)
|
||||
first_media = note.media[0]
|
||||
media_url = f"{site_url}/media/{first_media['path']}"
|
||||
item_xml += f"""
|
||||
<media:thumbnail url="{_escape_xml(media_url)}"/>"""
|
||||
|
||||
# Close item
|
||||
item_xml += """
|
||||
</item>
|
||||
"""
|
||||
yield item_xml
|
||||
@@ -273,6 +326,87 @@ def generate_rss_streaming(
|
||||
)
|
||||
|
||||
|
||||
def _inject_media_rss_elements(feed_xml: str, site_url: str, notes: list[Note]) -> str:
|
||||
"""
|
||||
Inject Media RSS elements into generated RSS feed
|
||||
|
||||
Adds media:content and media:thumbnail elements for notes with media using
|
||||
string manipulation. This approach is simpler than XML parsing and preserves
|
||||
the original formatting from feedgen.
|
||||
|
||||
Args:
|
||||
feed_xml: Generated RSS XML string
|
||||
site_url: Base site URL (no trailing slash)
|
||||
notes: List of notes (already reversed for feedgen)
|
||||
|
||||
Returns:
|
||||
Modified RSS XML with Media RSS elements
|
||||
"""
|
||||
# Step 1: Add Media RSS namespace to <rss> tag
|
||||
# Handle both possible attribute orderings from feedgen
|
||||
if '<rss xmlns:atom' in feed_xml:
|
||||
feed_xml = feed_xml.replace(
|
||||
'<rss xmlns:atom',
|
||||
'<rss xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom',
|
||||
1 # Only replace first occurrence
|
||||
)
|
||||
elif '<rss version="2.0"' in feed_xml:
|
||||
feed_xml = feed_xml.replace(
|
||||
'<rss version="2.0"',
|
||||
'<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"',
|
||||
1
|
||||
)
|
||||
else:
|
||||
# Fallback
|
||||
feed_xml = feed_xml.replace('<rss ', '<rss xmlns:media="http://search.yahoo.com/mrss/" ', 1)
|
||||
|
||||
# Step 2: Inject media elements for each note with media
|
||||
# We need to find each <enclosure> element and inject media elements after it
|
||||
# Notes are reversed in generate_rss, so notes[0] = first item in feed
|
||||
|
||||
for i, note in enumerate(notes):
|
||||
# Skip if note has no media
|
||||
if not hasattr(note, 'media') or not note.media:
|
||||
continue
|
||||
|
||||
# Build media elements for this note
|
||||
media_elements = []
|
||||
|
||||
# Add media:content for each image
|
||||
for media_item in note.media:
|
||||
media_url = f"{site_url}/media/{media_item['path']}"
|
||||
media_url_escaped = _escape_xml(media_url)
|
||||
mime_type = media_item.get('mime_type', 'image/jpeg')
|
||||
file_size = media_item.get('size', 0)
|
||||
|
||||
media_content = f'<media:content url="{media_url_escaped}" type="{mime_type}" medium="image" fileSize="{file_size}"/>'
|
||||
media_elements.append(media_content)
|
||||
|
||||
# Add media:thumbnail for first image
|
||||
first_media = note.media[0]
|
||||
media_url = f"{site_url}/media/{first_media['path']}"
|
||||
media_url_escaped = _escape_xml(media_url)
|
||||
media_thumbnail = f'<media:thumbnail url="{media_url_escaped}"/>'
|
||||
media_elements.append(media_thumbnail)
|
||||
|
||||
# Find the enclosure for this note and inject media elements after it
|
||||
# Look for the enclosure with the first media item's path
|
||||
enclosure_pattern = f'<enclosure url="{media_url_escaped}"'
|
||||
|
||||
if enclosure_pattern in feed_xml:
|
||||
# Find the end of the enclosure tag
|
||||
enclosure_pos = feed_xml.find(enclosure_pattern)
|
||||
enclosure_end = feed_xml.find('/>', enclosure_pos)
|
||||
|
||||
if enclosure_end != -1:
|
||||
# Inject media elements right after the enclosure tag
|
||||
insertion_point = enclosure_end + 2
|
||||
media_xml = ''.join(media_elements)
|
||||
feed_xml = feed_xml[:insertion_point] + media_xml + feed_xml[insertion_point:]
|
||||
|
||||
return feed_xml
|
||||
|
||||
|
||||
def _escape_xml(text: str) -> str:
|
||||
"""
|
||||
Escape special XML characters for safe inclusion in XML elements
|
||||
|
||||
Reference in New Issue
Block a user