diff --git a/starpunk/feeds/atom.py b/starpunk/feeds/atom.py index 8df3fc7..3935ef3 100644 --- a/starpunk/feeds/atom.py +++ b/starpunk/feeds/atom.py @@ -184,12 +184,18 @@ def generate_atom_streaming( yield f' \n' # Media enclosures (v1.2.0 Phase 3, per Q24 and ADR-057) + # Enhanced with title attribute for captions (v1.4.0 Phase 4) if hasattr(note, 'media') and note.media: for item in note.media: media_url = f"{site_url}/media/{item['path']}" mime_type = item.get('mime_type', 'image/jpeg') size = item.get('size', 0) - yield f' \n' + caption = item.get('caption', '') + + # Include title attribute for caption + title_attr = f' title="{_escape_xml(caption)}"' if caption else '' + + yield f' \n' # Content - include media as HTML (per Q24) if note.html: diff --git a/starpunk/feeds/json_feed.py b/starpunk/feeds/json_feed.py index 16b1783..a58348d 100644 --- a/starpunk/feeds/json_feed.py +++ b/starpunk/feeds/json_feed.py @@ -313,12 +313,46 @@ def _build_item_object(site_url: str, note: Note) -> Dict[str, Any]: if hasattr(note, 'tags') and note.tags: item["tags"] = [tag['display_name'] for tag in note.tags] - # Add custom StarPunk extensions - item["_starpunk"] = { + # Add custom StarPunk extensions (v1.4.0 Phase 4) + from flask import current_app + about_url = current_app.config.get( + "STARPUNK_ABOUT_URL", + "https://github.com/yourusername/starpunk" + ) + starpunk_ext = { "permalink_path": note.permalink, - "word_count": len(note.content.split()) + "word_count": len(note.content.split()), + "about": about_url } + # Add media variants if present + if hasattr(note, 'media') and note.media: + media_variants = [] + + for media_item in note.media: + variants = media_item.get('variants', {}) + + if variants: + media_info = { + "caption": media_item.get('caption', ''), + "variants": {} + } + + for variant_type, variant_data in variants.items(): + media_info["variants"][variant_type] = { + "url": f"{site_url}/media/{variant_data['path']}", + "width": variant_data['width'], + "height": variant_data['height'], + "size_in_bytes": variant_data['size_bytes'] + } + + media_variants.append(media_info) + + if media_variants: + starpunk_ext["media_variants"] = media_variants + + item["_starpunk"] = starpunk_ext + return item diff --git a/starpunk/feeds/rss.py b/starpunk/feeds/rss.py index 35153e7..80d72e9 100644 --- a/starpunk/feeds/rss.py +++ b/starpunk/feeds/rss.py @@ -304,18 +304,62 @@ def generate_rss_streaming( item_xml += f""" {_escape_xml(tag['display_name'])}""" - # Add media:content elements (all images) + # Enhanced media handling with variants (v1.4.0 Phase 4) 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""" - """ + variants = media_item.get('variants', {}) - # Add media:thumbnail (first image only) - first_media = note.media[0] - media_url = f"{site_url}/media/{first_media['path']}" - item_xml += f""" - """ + # Use media:group for multiple sizes of same image + if variants: + item_xml += '\n ' + + # Determine which variant is the default (largest available) + # Fallback order: large -> medium -> small + default_variant = None + for fallback in ['large', 'medium', 'small']: + if fallback in variants: + default_variant = fallback + break + + # Add each variant as media:content + for variant_type in ['large', 'medium', 'small']: + if variant_type in variants: + v = variants[variant_type] + media_url = f"{site_url}/media/{v['path']}" + is_default = 'true' if variant_type == default_variant else 'false' + item_xml += f''' + ''' + + item_xml += '\n ' + + # Add media:thumbnail + if 'thumb' in variants: + thumb = variants['thumb'] + thumb_url = f"{site_url}/media/{thumb['path']}" + item_xml += f''' + ''' + + # Add media:title for caption + if media_item.get('caption'): + item_xml += f''' + {_escape_xml(media_item['caption'])}''' + + else: + # Fallback for media without variants (legacy) + media_url = f"{site_url}/media/{media_item['path']}" + item_xml += f''' + ''' # Close item item_xml += """ diff --git a/tests/test_feeds_rss.py b/tests/test_feeds_rss.py index 22b7f35..80691ae 100644 --- a/tests/test_feeds_rss.py +++ b/tests/test_feeds_rss.py @@ -402,7 +402,7 @@ class TestRSSStreamingMedia: assert enclosure is not None def test_rss_streaming_includes_media_elements(self, app, note_with_single_media): - """Streaming RSS should include media:content and media:thumbnail""" + """Streaming RSS should include media:content and media:thumbnail (v1.4.0 with variants)""" with app.app_context(): generator = generate_rss_streaming( site_url="https://example.com", @@ -419,8 +419,10 @@ class TestRSSStreamingMedia: channel = root.find("channel") item = channel.find("item") - media_content = item.find("media:content", namespaces) - media_thumbnail = item.find("media:thumbnail", namespaces) + # v1.4.0: media:content is now inside media:group for images with variants + # Use // to search descendants, not direct children + media_content = item.find(".//media:content", namespaces) + media_thumbnail = item.find(".//media:thumbnail", namespaces) assert media_content is not None assert media_thumbnail is not None