# 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 `` tags within the `` CDATA section - Uses feedgen library for RSS 2.0 generation - No `` elements - No Media RSS namespace ### JSON Feed (`starpunk/feeds/json_feed.py`) - Includes media in `attachments` array (per JSON Feed 1.1 spec) - Includes media as `` tags in `content_html` - No top-level `image` field for items ### Note Model - Media accessed via `note.media` property (list of dicts) - Each media item has: `path`, `mime_type`, `size`, `caption` (optional) ## Design Goals 1. **Standards Compliance**: Follow Media RSS spec and JSON Feed 1.1 spec 2. **Backward Compatibility**: Keep existing HTML embedding for universal reader support 3. **Feed Reader Optimization**: Add structured metadata for enhanced display 4. **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 ```python # 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: '\n' ``` #### B. Add RSS `` Element (First Image Only) Per RSS 2.0 spec, only ONE enclosure per item is allowed. Use the first image. ```python # 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 `` and optional ``: ```python # 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: ```xml My Note Title https://example.com/note/my-slug https://example.com/note/my-slug Mon, 09 Dec 2024 12:00:00 +0000 Photo from today's hike Photo from today's hike

Note content here...

]]>
``` ### 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 ```python 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 ```json { "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": "
\"Photo\"\"

Note content here...

", "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: ```python # 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: 1. Update the opening RSS tag to include media namespace 2. Add `` element after `` 3. Add `` elements for each image 4. Add `` for first image 5. Add `` if caption exists ### JSON Feed Implementation Straightforward addition in `_build_item_object()`: ```python # 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) ```python 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 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 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 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 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) ```python 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): 1. **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) 2. **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 3. **JSON Feed Validator**: Use online JSON Feed validator - Verify `image` field accepted - Verify `attachments` array valid ### Integration Tests ```python 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 reference - `media:thumbnail` - Preview image - `media: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 1. **Implement RSS Changes** - Add namespace declaration - Add enclosure element - Add media:content elements - Add media:thumbnail - Add media:description for captions 2. **Implement JSON Feed Changes** - Add image field to item builder 3. **Add Tests** - Unit tests for both feed types - Integration tests for routes 4. **Manual Validation** - Test with W3C validator - Test in 3+ feed readers 5. **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