russell.ballestrini.net/lib/generate_jsonblog.py
russell@unturf.com 01b1c2056d Use Markdown content from index.md in blog.json instead of HTML
Updated generate_jsonblog.py to:
- Load Markdown content from output/<slug>/index.md files
- Set content_type: 'markdown' for each post
- Fall back to plain text if index.md not found

This ensures blog.json contains native Markdown content converted
from RST via pandoc, rather than stripped HTML from the Atom feed.

Works with the build order in make formats:
1. rst2md converts RST to Markdown
2. jsonblog reads the Markdown files
3. blog.json contains proper Markdown with content_type set

Result: 100/100 posts now have Markdown content with content_type field.
2026-01-19 15:05:20 -05:00

194 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""
Generate JSON Blog (jsonblog.dev) format - single JSON file with all posts.
Uses Markdown content from index.md files generated by rst2md.py.
"""
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 load_markdown_content(output_dir, slug):
"""Load markdown content from index.md file."""
md_path = output_dir / slug / 'index.md'
if md_path.exists():
try:
return md_path.read_text(encoding='utf-8')
except Exception as e:
print(f"Warning: Could not read {md_path}: {e}")
return None
return None
def generate_jsonblog(atom_path, output_dir, site_url):
"""Generate JSON Blog format with 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'],
}
# Try to load markdown content
md_content = load_markdown_content(output_dir, entry['slug'])
if md_content:
post['content'] = md_content
post['content_type'] = 'markdown'
markdown_found += 1
else:
# Fallback to plain text from HTML
post['content'] = entry['content']
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 content)")
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}")