- 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)
162 lines
4.5 KiB
Python
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}")
|