feat: Add tag/category support to all feed formats (v1.3.1)

Implements tag/category rendering in RSS 2.0, Atom 1.0, and JSON Feed 1.1
syndication feeds. Tags added in v1.3.0 are now visible to feed readers.

Changes:
- Load tags in feed generation (_get_cached_notes)
- Add <category> elements to RSS 2.0 feeds
- Add <category term/label> to Atom 1.0 feeds
- Add "tags" array to JSON Feed 1.1 items
- Omit empty tags field in JSON Feed (minimal approach)

Standards compliance:
- RSS 2.0: category element with display name
- Atom 1.0: RFC 4287 category with term and label attributes
- JSON Feed 1.1: tags array of strings

Per design: docs/design/v1.3.1/feed-tags-design.md
Implementation: docs/design/v1.3.1/2025-12-10-feed-tags-implementation.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-10 12:33:35 -07:00
parent 3222620cee
commit 9b26de7b05
5 changed files with 242 additions and 2 deletions

View File

@@ -178,6 +178,11 @@ def generate_atom_streaming(
# Link to entry
yield f' <link rel="alternate" type="text/html" href="{_escape_xml(permalink)}"/>\n'
# Add category elements for tags (v1.3.1)
if hasattr(note, 'tags') and note.tags:
for tag in note.tags:
yield f' <category term="{_escape_xml(tag["name"])}" label="{_escape_xml(tag["display_name"])}"/>\n'
# Media enclosures (v1.2.0 Phase 3, per Q24 and ADR-057)
if hasattr(note, 'media') and note.media:
for item in note.media:

View File

@@ -307,6 +307,12 @@ def _build_item_object(site_url: str, note: Note) -> Dict[str, Any]:
item["attachments"] = attachments
# Add tags array (v1.3.1)
# Per spec: array of plain strings (tags, not categories)
# Omit field when no tags (user decision: no empty array)
if hasattr(note, 'tags') and note.tags:
item["tags"] = [tag['display_name'] for tag in note.tags]
# Add custom StarPunk extensions
item["_starpunk"] = {
"permalink_path": note.permalink,

View File

@@ -142,6 +142,11 @@ def generate_rss(
# feedgen automatically wraps content in CDATA for RSS
fe.description(html_content)
# Add category elements for tags (v1.3.1)
if hasattr(note, 'tags') and note.tags:
for tag in note.tags:
fe.category({'term': tag['display_name']})
# Add RSS enclosure element (first image only, per RSS 2.0 spec)
if hasattr(note, 'media') and note.media:
first_media = note.media[0]
@@ -293,6 +298,12 @@ def generate_rss_streaming(
item_xml += f"""
<description><![CDATA[{html_content}]]></description>"""
# Add category elements for tags (v1.3.1)
if hasattr(note, 'tags') and note.tags:
for tag in note.tags:
item_xml += f"""
<category>{_escape_xml(tag['display_name'])}</category>"""
# Add media:content elements (all images)
if hasattr(note, 'media') and note.media:
for media_item in note.media: