## 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>
13 KiB
Feed Media Enhancement Design: Option 2 (RSS + Media RSS Extension)
Overview
This design document specifies the implementation of Option 2 for feed media support: adding Media RSS namespace elements to RSS feeds and the image field to JSON Feed items. This provides improved feed reader compatibility for notes with attached images.
Target Version: v1.2.x Estimated Effort: 4-6 hours Prerequisites: Media attachment model implemented (ADR-057)
Current State
RSS Feed (starpunk/feeds/rss.py)
- Embeds media as
<img>tags within the<description>CDATA section - Uses feedgen library for RSS 2.0 generation
- No
<enclosure>elements - No Media RSS namespace
JSON Feed (starpunk/feeds/json_feed.py)
- Includes media in
attachmentsarray (per JSON Feed 1.1 spec) - Includes media as
<img>tags incontent_html - No top-level
imagefield for items
Note Model
- Media accessed via
note.mediaproperty (list of dicts) - Each media item has:
path,mime_type,size,caption(optional)
Design Goals
- Standards Compliance: Follow Media RSS spec and JSON Feed 1.1 spec
- Backward Compatibility: Keep existing HTML embedding for universal reader support
- Feed Reader Optimization: Add structured metadata for enhanced display
- Minimal Changes: Modify only feed generation, no database changes
Files to Modify
1. starpunk/feeds/rss.py
Changes Required:
A. Add Media RSS Namespace to Feed Generator
Location: generate_rss() function and generate_rss_streaming() function
# Add namespace registration before generating XML
# For feedgen-based generation:
fg.load_extension('media', rss=True) # feedgen has built-in media extension
# For streaming generation, add to opening RSS tag:
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">\n'
B. Add RSS <enclosure> Element (First Image Only)
Per RSS 2.0 spec, only ONE enclosure per item is allowed. Use the first image.
# In item generation, after setting description:
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')
)
C. Add Media RSS Elements (All Images)
For each image, add <media:content> and optional <media:description>:
# Using feedgen's media extension:
for media_item in note.media:
media_url = f"{site_url}/media/{media_item['path']}"
# Add media:content
fe.media.content({
'url': media_url,
'type': media_item.get('mime_type', 'image/jpeg'),
'medium': 'image',
'fileSize': str(media_item.get('size', 0))
})
# Add media:description if caption exists
if media_item.get('caption'):
fe.media.description(media_item['caption'], type='plain')
# Add media:thumbnail for first image
if note.media:
first_media = note.media[0]
fe.media.thumbnail({
'url': f"{site_url}/media/{first_media['path']}"
})
D. Expected XML Output Structure
For an item with 2 images:
<item>
<title>My Note Title</title>
<link>https://example.com/note/my-slug</link>
<guid isPermaLink="true">https://example.com/note/my-slug</guid>
<pubDate>Mon, 09 Dec 2024 12:00:00 +0000</pubDate>
<!-- Standard RSS enclosure (first image only) -->
<enclosure url="https://example.com/media/2024/12/image1.jpg"
length="245760"
type="image/jpeg"/>
<!-- Media RSS elements (all images) -->
<media:content url="https://example.com/media/2024/12/image1.jpg"
type="image/jpeg"
medium="image"
fileSize="245760"/>
<media:content url="https://example.com/media/2024/12/image2.jpg"
type="image/jpeg"
medium="image"
fileSize="198432"/>
<!-- Thumbnail (first image) -->
<media:thumbnail url="https://example.com/media/2024/12/image1.jpg"/>
<!-- Caption if present -->
<media:description type="plain">Photo from today's hike</media:description>
<!-- Description with embedded HTML (for legacy readers) -->
<description><![CDATA[
<div class="media">
<img src="https://example.com/media/2024/12/image1.jpg" alt="Photo from today's hike" />
<img src="https://example.com/media/2024/12/image2.jpg" alt="" />
</div>
<p>Note content here...</p>
]]></description>
</item>
2. starpunk/feeds/json_feed.py
Changes Required:
A. Add image Field to Item Objects
Per JSON Feed 1.1 spec, image is "the URL of the main image for the item."
Location: _build_item_object() function
def _build_item_object(site_url: str, note: Note) -> Dict[str, Any]:
# ... existing code ...
# Add image field (URL of first/main image)
# Per JSON Feed 1.1: "the URL of the main image for the item"
if hasattr(note, 'media') and note.media:
first_media = note.media[0]
item["image"] = f"{site_url}/media/{first_media['path']}"
# ... rest of existing code (content_html, attachments, etc.) ...
B. Expected JSON Output Structure
{
"id": "https://example.com/note/my-slug",
"url": "https://example.com/note/my-slug",
"title": "My Note Title",
"date_published": "2024-12-09T12:00:00Z",
"image": "https://example.com/media/2024/12/image1.jpg",
"content_html": "<div class=\"media\"><img src=\"https://example.com/media/2024/12/image1.jpg\" alt=\"Photo from today's hike\" /><img src=\"https://example.com/media/2024/12/image2.jpg\" alt=\"\" /></div><p>Note content here...</p>",
"attachments": [
{
"url": "https://example.com/media/2024/12/image1.jpg",
"mime_type": "image/jpeg",
"title": "Photo from today's hike",
"size_in_bytes": 245760
},
{
"url": "https://example.com/media/2024/12/image2.jpg",
"mime_type": "image/jpeg",
"size_in_bytes": 198432
}
],
"_starpunk": {
"permalink_path": "/note/my-slug",
"word_count": 42
}
}
Implementation Details
RSS Implementation: feedgen vs Manual Streaming
For generate_rss() (feedgen-based):
The feedgen library has a media extension. Check if it's available:
# Test if feedgen supports media extension
from feedgen.ext.media import MediaExtension
# If supported, use:
fg.register_extension('media', MediaExtension, rss=True)
If feedgen's media extension is insufficient, consider manual XML injection after feedgen generates the base XML.
For generate_rss_streaming() (manual XML):
Modify the streaming generator to include media elements. This requires:
- Update the opening RSS tag to include media namespace
- Add
<enclosure>element after<pubDate> - Add
<media:content>elements for each image - Add
<media:thumbnail>for first image - Add
<media:description>if caption exists
JSON Feed Implementation
Straightforward addition in _build_item_object():
# Add image field if media exists
if hasattr(note, 'media') and note.media:
first_media = note.media[0]
item["image"] = f"{site_url}/media/{first_media['path']}"
Testing Requirements
Unit Tests to Add/Modify
File: tests/test_feeds_rss.py (create or extend)
def test_rss_enclosure_for_note_with_media():
"""RSS item should include enclosure element for first image."""
# Create note with media
# Generate RSS
# Parse XML, verify <enclosure> present with correct attributes
def test_rss_media_content_for_all_images():
"""RSS item should include media:content for each image."""
# Create note with 2 images
# Generate RSS
# Parse XML, verify 2 <media:content> elements
def test_rss_media_thumbnail_for_first_image():
"""RSS item should include media:thumbnail for first image."""
# Create note with media
# Generate RSS
# Parse XML, verify <media:thumbnail> present
def test_rss_media_description_for_caption():
"""RSS item should include media:description if caption exists."""
# Create note with captioned image
# Generate RSS
# Parse XML, verify <media:description> present
def test_rss_no_media_elements_without_attachments():
"""RSS item without media should have no media elements."""
# Create note without media
# Generate RSS
# Parse XML, verify no enclosure or media:* elements
def test_rss_namespace_declaration():
"""RSS feed should declare media namespace."""
# Generate any RSS feed
# Verify xmlns:media attribute in root element
File: tests/test_feeds_json.py (create or extend)
def test_json_feed_image_field_for_note_with_media():
"""JSON Feed item should include image field for first image."""
# Create note with media
# Generate JSON feed
# Parse JSON, verify "image" field present with correct URL
def test_json_feed_no_image_field_without_media():
"""JSON Feed item without media should not have image field."""
# Create note without media
# Generate JSON feed
# Parse JSON, verify "image" field not present
def test_json_feed_image_uses_first_media():
"""JSON Feed image field should use first media item URL."""
# Create note with 3 images
# Generate JSON feed
# Verify "image" URL matches first image path
Feed Validation Tests
Manual Validation (document in test plan):
-
W3C Feed Validator: https://validator.w3.org/feed/
- Submit generated RSS feed
- Verify no errors for media:* elements
- Note: Validator may warn about unknown extensions (acceptable)
-
Feed Reader Testing:
- Feedly: Verify images display in article preview
- NetNewsWire: Check media thumbnail in list view
- Feedbin: Test image extraction
- RSS.app: Verify enclosure handling
-
JSON Feed Validator: Use online JSON Feed validator
- Verify
imagefield accepted - Verify
attachmentsarray valid
- Verify
Integration Tests
def test_rss_route_with_media_notes(client, app):
"""GET /feed.xml with media notes returns valid RSS with media elements."""
# Create test notes with media
# Request /feed.xml
# Verify response contains media namespace and elements
def test_json_route_with_media_notes(client, app):
"""GET /feed.json with media notes returns JSON with image fields."""
# Create test notes with media
# Request /feed.json
# Verify response contains image fields
Reference Documentation
Media RSS Specification
- URL: https://www.rssboard.org/media-rss
- Key Elements Used:
media:content- Primary media referencemedia:thumbnail- Preview imagemedia:description- Caption text
JSON Feed 1.1 Specification
- URL: https://jsonfeed.org/version/1.1/
- Key Fields Used:
image(item level) - "the URL of the main image for the item"attachments- Array of attachment objects (already implemented)
RSS 2.0 Enclosure Specification
- URL: https://www.rssboard.org/rss-specification#ltenclosuregtSubelementOfLtitemgt
- Constraint: Only ONE enclosure per item allowed
- Required Attributes:
url,length,type
Feed Reader Compatibility Notes
Media RSS Support
| Reader | media:content | media:thumbnail | enclosure |
|---|---|---|---|
| Feedly | Yes | Yes | Yes |
| Inoreader | Yes | Yes | Yes |
| NetNewsWire | Partial | Yes | Yes |
| Feedbin | Yes | Yes | Yes |
| RSS.app | Yes | Yes | Yes |
| The Old Reader | Yes | Partial | Yes |
JSON Feed Image Support
| Reader | image field | attachments |
|---|---|---|
| Feedly | Yes | Yes |
| NetNewsWire | Yes | Yes |
| Reeder | Yes | Yes |
| Feedbin | Yes | Yes |
Note: The HTML-embedded images in description/content_html serve as fallback for readers that don't support Media RSS or JSON Feed attachments.
Rollout Plan
-
Implement RSS Changes
- Add namespace declaration
- Add enclosure element
- Add media:content elements
- Add media:thumbnail
- Add media:description for captions
-
Implement JSON Feed Changes
- Add image field to item builder
-
Add Tests
- Unit tests for both feed types
- Integration tests for routes
-
Manual Validation
- Test with W3C validator
- Test in 3+ feed readers
-
Deploy
- Release as part of v1.2.x
Future Considerations (Option 3)
This design explicitly does NOT include:
- Multiple image sizes/thumbnails (deferred to ADR-059)
- Video support (deferred to v1.4.0)
- Audio/podcast support (deferred to v1.3.0+)
- Full Media RSS attribute set (width, height, duration)
These are documented in ADR-059: Full Feed Media Standardization for future releases.
Summary of Changes
| File | Change |
|---|---|
starpunk/feeds/rss.py |
Add media namespace, enclosure, media:content, media:thumbnail, media:description |
starpunk/feeds/json_feed.py |
Add image field to items with media |
tests/test_feeds_rss.py |
Add 6 new test cases for media elements |
tests/test_feeds_json.py |
Add 3 new test cases for image field |
Total Estimated Changes: ~100-150 lines of new code + ~100 lines of tests