russell.ballestrini.net/lib/generate_jsonblog.py
russell@unturf.com e1fdae237e Generate index-slim.md without metadata for blog.json
- rst2md.py now creates both index.md (with metadata) and index-slim.md (clean)
- generate_jsonblog.py references index-slim.md for clean content
- Fixes "Error: No content found" in blog viewer
- strip_pelican_metadata() removes title and metadata fields
2026-01-20 06:47:11 -05:00

179 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Generate JSON Blog (jsonblog.dev) format - single JSON file with all posts.
Links to Markdown files (index.md) generated by rst2md.py instead of embedding content.
"""
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
import re
def strip_html(html):
"""Remove HTML tags and decode entities."""
if not html:
return ''
# Remove HTML tags
text = re.sub(r'<[^>]+>', '', html)
# Decode common HTML entities
text = text.replace('&amp;', '&')
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&quot;', '"')
text = text.replace('&#39;', "'")
text = text.replace('&nbsp;', ' ')
# Collapse whitespace
text = re.sub(r'\s+', ' ', text).strip()
return text
def parse_atom_feed(atom_path):
"""Parse Atom feed and extract entries."""
tree = ET.parse(atom_path)
root = tree.getroot()
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 '',
'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 = {
'title': '',
'slug': '',
'content': '',
'content_html': '',
'summary': '',
'createdAt': '',
'updatedAt': '',
'tags': [],
'url': '',
}
# URL and slug
for link in entry.findall('atom:link', ns):
if link.get('rel') == 'alternate':
item['url'] = link.get('href', '')
# Extract slug from URL
slug_match = re.search(r'/([^/]+)/?$', item['url'])
if slug_match:
item['slug'] = slug_match.group(1)
break
# Title
title_elem = entry.find('atom:title', ns)
if title_elem is not None:
item['title'] = title_elem.text
# Content (will be replaced with markdown later)
content_elem = entry.find('atom:content', ns)
if content_elem is not None:
item['content_html'] = content_elem.text or ''
item['content'] = strip_html(item['content_html'])
# Summary
summary_elem = entry.find('atom:summary', ns)
if summary_elem is not None:
item['summary'] = summary_elem.text or ''
# Dates (convert to YYYY-MM-DD for JSON Blog format)
published_elem = entry.find('atom:published', ns)
if published_elem is not None:
date_str = published_elem.text
if date_str:
item['createdAt'] = date_str[:10] # YYYY-MM-DD
updated_elem = entry.find('atom:updated', ns)
if updated_elem is not None:
date_str = updated_elem.text
if date_str:
item['updatedAt'] = date_str[:10]
# Tags
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_jsonblog(atom_path, output_dir, site_url):
"""Generate JSON Blog format with links to Markdown content."""
feed_info, entries = parse_atom_feed(atom_path)
jsonblog = {
'site': {
'title': feed_info['title'],
'description': feed_info['subtitle'],
'url': site_url,
},
'basics': {
'name': feed_info['author'] or 'Russell Ballestrini',
'url': site_url,
},
'posts': []
}
markdown_found = 0
for entry in entries:
post = {
'title': entry['title'],
'slug': entry['slug'],
'createdAt': entry['createdAt'],
}
# Check if slim markdown file exists (without metadata), then link to it
md_path = output_dir / entry['slug'] / 'index-slim.md'
if md_path.exists():
post['content_url'] = f"{site_url}/{entry['slug']}/index-slim.md"
post['content_type'] = 'markdown'
markdown_found += 1
if entry['updatedAt'] and entry['updatedAt'] != entry['createdAt']:
post['updatedAt'] = entry['updatedAt']
if entry['tags']:
post['tags'] = entry['tags']
if entry['summary']:
post['summary'] = entry['summary']
if entry['url']:
post['url'] = entry['url']
jsonblog['posts'].append(post)
print(f"Generated {len(jsonblog['posts'])} posts ({markdown_found} with markdown links)")
return jsonblog
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: generate_jsonblog.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)
jsonblog = generate_jsonblog(atom_path, output_dir, site_url)
output_path = output_dir / 'blog.json'
output_path.write_text(json.dumps(jsonblog, indent=2, ensure_ascii=False))
print(f"Generated {output_path}")