#!/usr/bin/env python3
# Static Blog Generator for Neocities (or any other web host)
# Converts individual HTML files into a full blog with RSS support.
# Version 1.0 (11 May 2026) - Blue Oak Model License 1.0.0
# Project page: https://640kb.neocities.org/apps/apps.html
# Requirements: python 3.6 or above
import os
import re
import glob
from datetime import datetime
from xml.sax.saxutils import escape as xml_escape
# ===========================================================================
# CONFIGURATION: Change these values as needed. I recommend keeping the core
# defaults during your first testing (except SCRIPT_TITLE, of course).
# ===========================================================================
# Base URL for your blog (used in RSS feed links)
# Change this if your blog is not located at /blog/
BLOG_BASE_URL = "/blog/"
# Number of posts per page
POSTS_PER_PAGE = 20
# Number of recent posts to include in RSS feed
RSS_ITEMS = 10
# Directory containing your post fragments
POSTS_DIR = "posts"
# Your HTML template file
TEMPLATE_FILE = "template.html"
# Script title printed to terminal output after index*.html/rss creation
SCRIPT_TITLE = "640KB Static Blog Generator"
# ===========================================================================
def clean_old_index_files():
for filepath in glob.glob("index*.html"):
os.remove(filepath)
print(f"Removed old file: {filepath}")
def get_all_posts():
posts = []
pattern = os.path.join(POSTS_DIR, "*", "*.html")
for filepath in glob.glob(pattern):
year = os.path.basename(os.path.dirname(filepath))
filename = os.path.basename(filepath)
match = re.match(r"(\d{2})\.(\d{2})--", filename)
if not match:
print(f"Warning: Skipping {filepath} - filename doesn't start with MM.DD--")
continue
month, day = match.group(1), match.group(2)
try:
post_date = datetime(int(year), int(month), int(day))
except ValueError:
print(f"Warning: Skipping {filepath} - invalid date {year}-{month}-{day}")
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
posts.append({
'year': year,
'month': month,
'day': day,
'date_obj': post_date,
'filename': filename,
'filepath': filepath,
'content': content
})
posts.sort(key=lambda x: x['date_obj'], reverse=True)
return posts
def render_posts(posts_slice, start_index=1, total_posts=0, current_page=1, total_pages=1):
html_parts = []
for post in posts_slice:
post_slug = post['filename'].replace('.html', '')
post_id = f"post-{post['date_obj'].strftime('%Y%m%d')}-{post_slug}"
html_parts.append(f'