diff --git a/config.py b/config.py index 2f193d3..16f4d49 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,11 @@ """Configuration file for picopaper blog""" +# General site settings BLOG_TITLE = "PicoPaper.com" BLOG_DESCRIPTION = "we like simple." +BASE_URL = "https://picopaper.com" # Your site's base URL (no trailing slash) +AUTHOR_NAME = "PicoPaper" +AUTHOR_EMAIL = "hello@picopaper.com" # Optional THEME = "default" # Exclude specific feeds from the main page (they'll still have their own /feed/name/ pages) @@ -17,5 +21,10 @@ NAVBAR_ITEMS = [ # Logo settings HIDE_LOGO = False HIDE_TITLE = True -LOGO_PATH = "/images/logo.png" +LOGO_PATH = "/images/logo.png" + +# Feed settings +ENABLE_RSS_FEED = True +RSS_FEED_PATH = "rss.xml" # Path relative to site root (e.g., "rss.xml" or "feed/rss.xml") +FEED_MAX_ITEMS = 20 # Maximum number of items to include in feeds diff --git a/picopaper.py b/picopaper.py index 700bc67..f06b157 100644 --- a/picopaper.py +++ b/picopaper.py @@ -6,7 +6,10 @@ from datetime import datetime from pathlib import Path from jinja2 import Environment, FileSystemLoader import markdown -from config import BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_FEEDS_FROM_MAIN, NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH +from config import (BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_FEEDS_FROM_MAIN, + NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH, + ENABLE_RSS_FEED, RSS_FEED_PATH, + BASE_URL, AUTHOR_NAME, AUTHOR_EMAIL, FEED_MAX_ITEMS) class SSGGGenerator: def __init__(self, items_dir='items', output_dir='output', theme=None, blog_title=None, blog_description=None): @@ -149,7 +152,9 @@ class SSGGGenerator: all_posts=all_posts or posts, hide_logo=self.hide_logo, hide_title=self.hide_title, - logo_path=self.logo_path + logo_path=self.logo_path, + rss_feed_enabled=ENABLE_RSS_FEED, + rss_feed_path=RSS_FEED_PATH ) output_path.parent.mkdir(parents=True, exist_ok=True) @@ -183,7 +188,9 @@ class SSGGGenerator: all_posts=all_posts or [], hide_logo=self.hide_logo, hide_title=self.hide_title, - logo_path=self.logo_path + logo_path=self.logo_path, + rss_feed_enabled=ENABLE_RSS_FEED, + rss_feed_path=RSS_FEED_PATH ) output_path.parent.mkdir(parents=True, exist_ok=True) @@ -205,7 +212,9 @@ class SSGGGenerator: all_posts=all_posts or [], hide_logo=self.hide_logo, hide_title=self.hide_title, - logo_path=self.logo_path + logo_path=self.logo_path, + rss_feed_enabled=ENABLE_RSS_FEED, + rss_feed_path=RSS_FEED_PATH ) # Create directory for the post slug @@ -219,6 +228,82 @@ class SSGGGenerator: print(f"✓ Generated {output_path}") + def generate_rss_feed(self, posts): + """Generate RSS 2.0 feed for main feed posts""" + from xml.etree.ElementTree import Element, SubElement, tostring, register_namespace + from xml.dom import minidom + import re + + # Register atom namespace to avoid ns0 prefix + register_namespace('atom', 'http://www.w3.org/2005/Atom') + + # Limit posts + posts = posts[:FEED_MAX_ITEMS] + + # Build feed URL correctly - ensure no double slashes + feed_path = RSS_FEED_PATH.lstrip('/') + # Remove trailing slash from BASE_URL if present for clean URL construction + base_url_clean = BASE_URL.rstrip('/') + feed_url = f"{base_url_clean}/{feed_path}" + + # Create RSS element (namespace will be added automatically when we use atom:link) + rss = Element('rss', version='2.0') + channel = SubElement(rss, 'channel') + + # Channel metadata + SubElement(channel, 'title').text = self.blog_title + SubElement(channel, 'description').text = self.blog_description + SubElement(channel, 'link').text = base_url_clean + + # Add atom:link with rel="self" (required by RSS best practices) + atom_link = SubElement(channel, '{http://www.w3.org/2005/Atom}link') + atom_link.set('href', feed_url) + atom_link.set('rel', 'self') + atom_link.set('type', 'application/rss+xml') + + SubElement(channel, 'lastBuildDate').text = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +0000') + + # Add author information (managingEditor format: email (name)) + if AUTHOR_EMAIL and AUTHOR_NAME: + SubElement(channel, 'managingEditor').text = f"{AUTHOR_EMAIL} ({AUTHOR_NAME})" + elif AUTHOR_EMAIL: + SubElement(channel, 'managingEditor').text = AUTHOR_EMAIL + + # Helper function to convert relative URLs to absolute + def make_absolute_urls(html_content): + # Replace relative URLs with absolute ones + html_content = re.sub(r'href="/', f'href="{base_url_clean}/', html_content) + html_content = re.sub(r'src="/', f'src="{base_url_clean}/', html_content) + return html_content + + # Add items + for post in posts: + item = SubElement(channel, 'item') + SubElement(item, 'title').text = post['title'] + SubElement(item, 'link').text = f"{base_url_clean}{post['url']}" + SubElement(item, 'guid', isPermaLink='true').text = f"{base_url_clean}{post['url']}" + SubElement(item, 'pubDate').text = datetime.strptime(post['date'], '%Y-%m-%d').strftime('%a, %d %b %Y 00:00:00 +0000') + + # Content type based on post type + if post['type'] == 'long': + # For long posts, just show title/summary + SubElement(item, 'description').text = f"Read more at {base_url_clean}{post['url']}" + else: + # For short posts, include full content with absolute URLs + content_absolute = make_absolute_urls(post['content']) + SubElement(item, 'description').text = content_absolute + + # Pretty print XML + xml_str = minidom.parseString(tostring(rss, encoding='utf-8')).toprettyxml(indent=' ', encoding='utf-8') + + # Write to file + output_path = self.output_dir / RSS_FEED_PATH + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'wb') as f: + f.write(xml_str) + + print(f"✓ Generated {output_path}") + def copy_assets(self): """Copy theme assets and images to output directory""" import shutil @@ -290,6 +375,10 @@ class SSGGGenerator: if post['type'] in ['long', 'short', 'page']: self.generate_post_page(post, all_posts=feed_posts) + # Generate RSS feed + if ENABLE_RSS_FEED: + self.generate_rss_feed(feed_posts) + # Copy assets self.copy_assets() diff --git a/theme/default/templates/meta.tmpl b/theme/default/templates/meta.tmpl index 6466300..25c6e67 100644 --- a/theme/default/templates/meta.tmpl +++ b/theme/default/templates/meta.tmpl @@ -4,3 +4,4 @@