russell.ballestrini.net/lib/generate_jsonfeed.py
russell@unturf.com 7c91647a55 Add lib/ format generation scripts for agent-friendly formats
- lib/rst2md.py - RST to Markdown converter using pandoc
- lib/generate_jsonfeed.py - JSON Feed v1.1 generator
- lib/generate_jsonblog.py - JSON Blog format (jsonblog.dev)
- lib/generate_plaintext.py - HTML to plain text converter
- lib/generate_llms_txt.py - AI agent onboarding file

These scripts generate multiple formats for AI/LLM consumption:
- blog.json at root (entire blog in one file)
- feeds/all.json (JSON Feed format)
- llms.txt (onboarding documentation)
- index.md (Markdown versions)
- index.txt (plain text versions)
2026-01-19 14:36:20 -05:00

162 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""
Generate JSON Feed (jsonfeed.org) from Pelican Atom feed.
"""
import json
import sys
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path
def parse_atom_feed(atom_path):
"""Parse Atom feed and extract entries."""
tree = ET.parse(atom_path)
root = tree.getroot()
# Atom namespace
ns = {'atom': 'http://www.w3.org/2005/Atom'}
feed_info = {
'title': root.find('atom:title', ns).text if root.find('atom:title', ns) is not None else '',
'subtitle': root.find('atom:subtitle', ns).text if root.find('atom:subtitle', ns) is not None else '',
'link': '',
'author': '',
}
# Get feed link
for link in root.findall('atom:link', ns):
if link.get('rel') == 'alternate':
feed_info['link'] = link.get('href', '')
break
# Get author
author_elem = root.find('atom:author/atom:name', ns)
if author_elem is not None:
feed_info['author'] = author_elem.text
entries = []
for entry in root.findall('atom:entry', ns):
item = {
'id': '',
'url': '',
'title': '',
'content_html': '',
'summary': '',
'date_published': '',
'date_modified': '',
'tags': [],
}
# ID
id_elem = entry.find('atom:id', ns)
if id_elem is not None:
item['id'] = id_elem.text
# URL
for link in entry.findall('atom:link', ns):
if link.get('rel') == 'alternate':
item['url'] = link.get('href', '')
break
# Title
title_elem = entry.find('atom:title', ns)
if title_elem is not None:
item['title'] = title_elem.text
# Content
content_elem = entry.find('atom:content', ns)
if content_elem is not None:
item['content_html'] = content_elem.text or ''
# Summary
summary_elem = entry.find('atom:summary', ns)
if summary_elem is not None:
item['summary'] = summary_elem.text or ''
# Dates
published_elem = entry.find('atom:published', ns)
if published_elem is not None:
item['date_published'] = published_elem.text
updated_elem = entry.find('atom:updated', ns)
if updated_elem is not None:
item['date_modified'] = updated_elem.text
# Tags/categories
for category in entry.findall('atom:category', ns):
term = category.get('term')
if term:
item['tags'].append(term)
entries.append(item)
return feed_info, entries
def generate_jsonfeed(atom_path, site_url):
"""Generate JSON Feed from Atom feed."""
feed_info, entries = parse_atom_feed(atom_path)
jsonfeed = {
'version': 'https://jsonfeed.org/version/1.1',
'title': feed_info['title'],
'home_page_url': site_url,
'feed_url': f'{site_url}/feeds/all.json',
'description': feed_info['subtitle'],
'language': 'en-US',
'authors': [
{
'name': feed_info['author'] or 'Russell Ballestrini',
'url': site_url,
}
],
'items': []
}
for entry in entries:
item = {
'id': entry['id'],
'url': entry['url'],
'title': entry['title'],
}
if entry['content_html']:
item['content_html'] = entry['content_html']
if entry['summary']:
item['summary'] = entry['summary']
if entry['date_published']:
item['date_published'] = entry['date_published']
if entry['date_modified']:
item['date_modified'] = entry['date_modified']
if entry['tags']:
item['tags'] = entry['tags']
jsonfeed['items'].append(item)
return jsonfeed
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: generate_jsonfeed.py <output_directory> [site_url]")
sys.exit(1)
output_dir = Path(sys.argv[1])
site_url = sys.argv[2] if len(sys.argv) > 2 else 'https://russell.ballestrini.net'
atom_path = output_dir / 'feeds' / 'all.atom.xml'
if not atom_path.exists():
print(f"Error: Atom feed not found at {atom_path}")
sys.exit(1)
jsonfeed = generate_jsonfeed(atom_path, site_url)
output_path = output_dir / 'feeds' / 'all.json'
output_path.write_text(json.dumps(jsonfeed, indent=2, ensure_ascii=False))
print(f"Generated {output_path}")