A community based topic aggregation platform built on atproto

feat(aggregator): add source name field and improve formatting

- Add source_name field to Perspective model for better attribution
- Extract source name from HTML anchor tags in parser
- Display source name in rich text (e.g., "The Straits Times" vs generic "Source")
- Improve spacing in highlights, perspectives, and sources lists (double newlines)
- Better visual separation between list items

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

Co-Authored-By: Claude <noreply@anthropic.com>

+18 -10
+6 -3
aggregators/kagi-news/src/html_parser.py
··· 78 78 Perspective( 79 79 actor=p['actor'], 80 80 description=p['description'], 81 - source_url=p['source_url'] 81 + source_url=p['source_url'], 82 + source_name=p.get('source_name', '') 82 83 ) 83 84 for p in parsed['perspectives'] 84 85 ] ··· 230 231 actor, rest = full_text.split(':', 1) 231 232 actor = actor.strip() 232 233 233 - # Find the <a> tag for source URL 234 + # Find the <a> tag for source URL and name 234 235 a_tag = li.find('a') 235 236 source_url = a_tag['href'] if a_tag and a_tag.get('href') else "" 237 + source_name = a_tag.get_text(strip=True) if a_tag else "" 236 238 237 239 # Extract description (between colon and source link) 238 240 # Remove the source citation part in parentheses ··· 250 252 return { 251 253 'actor': actor, 252 254 'description': description, 253 - 'source_url': source_url 255 + 'source_url': source_url, 256 + 'source_name': source_name 254 257 } 255 258 256 259 def _extract_sources(self, soup: BeautifulSoup) -> List[Dict]:
+1
aggregators/kagi-news/src/models.py
··· 20 20 actor: str 21 21 description: str 22 22 source_url: str 23 + source_name: str = "" # Name of the source (e.g., "The Straits Times") 23 24 24 25 25 26 @dataclass
+11 -7
aggregators/kagi-news/src/richtext_formatter.py
··· 42 42 builder.add_bold("Highlights:") 43 43 builder.add_text("\n") 44 44 for highlight in story.highlights: 45 - builder.add_text(f"• {highlight}\n") 45 + builder.add_text(f"• {highlight}\n\n") 46 46 builder.add_text("\n") 47 47 48 48 # Perspectives (if present) ··· 53 53 # Bold the actor name 54 54 actor_with_colon = f"{perspective.actor}:" 55 55 builder.add_bold(actor_with_colon) 56 - builder.add_text(f" {perspective.description} (") 56 + builder.add_text(f" {perspective.description}") 57 57 58 - # Add link to source 59 - source_link_text = "Source" 60 - builder.add_link(source_link_text, perspective.source_url) 61 - builder.add_text(")\n") 58 + # Add link to source if available 59 + if perspective.source_url: 60 + builder.add_text(" (") 61 + source_link_text = perspective.source_name if perspective.source_name else "Source" 62 + builder.add_link(source_link_text, perspective.source_url) 63 + builder.add_text(")") 64 + 65 + builder.add_text("\n\n") 62 66 builder.add_text("\n") 63 67 64 68 # Quote (if present) ··· 74 78 for source in story.sources: 75 79 builder.add_text("• ") 76 80 builder.add_link(source.title, source.url) 77 - builder.add_text(f" - {source.domain}\n") 81 + builder.add_text(f" - {source.domain}\n\n") 78 82 builder.add_text("\n") 79 83 80 84 # Kagi News attribution