Feeds
+-
+ {% for feed in feeds %}
+
- {{ feed.name }} ({{ feed.count }} posts) + {% endfor %} +
diff --git a/README.md b/README.md index 1f79def..549d0e8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ + + # picopaper A minimal static site generator for blogs built with Python 3 and Jinja2 - Status: alpha - expect many changes +- [Issue Tracker](https://git.uphillsecurity.com/cf7/picopaper/issues) - Goals: keeping it simple and easy to understand - Demo: [picopaper.com](https://picopaper.com/) @@ -23,6 +26,7 @@ Show cases: - separate feeds (used for categories, tagging, etc) `/feed/{tag}` - exclusion of feeds from main feed (drafts or system notes) - HTML anchors for headers +- list random posts at the bottom **Ideas**: - RSS @@ -157,6 +161,12 @@ To switch themes, change the `THEME` setting in `config.py` --- +## Notes + +- [Github Mirror](https://github.com/CaffeineFueled1/picopaper) + +--- + ## Security For security concerns or reports, please contact via `hello a t uphillsecurity d o t com` [gpg](https://uphillsecurity.com/gpg). diff --git a/config.py b/config.py index b7976d8..2f193d3 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ """Configuration file for picopaper blog""" -BLOG_TITLE = "picopaper" +BLOG_TITLE = "PicoPaper.com" BLOG_DESCRIPTION = "we like simple." THEME = "default" @@ -10,6 +10,12 @@ EXCLUDE_FEEDS_FROM_MAIN = ['draft','private'] # e.g., ['python', 'drafts'] # Navigation bar items - list of dictionaries with 'text' and 'url' keys NAVBAR_ITEMS = [ {'text': 'Home', 'url': '/'}, + {'text': 'Feeds', 'url': '/feed/'}, {'text': 'About', 'url': '/about/'} ] +# Logo settings +HIDE_LOGO = False +HIDE_TITLE = True +LOGO_PATH = "/images/logo.png" + diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..5f00575 Binary files /dev/null and b/images/logo.png differ diff --git a/items/2025-10-09_short_features.md b/items/2025-10-09_short_features.md index e521d99..78dffec 100644 --- a/items/2025-10-09_short_features.md +++ b/items/2025-10-09_short_features.md @@ -7,6 +7,7 @@ - excluding feeds in config file like [/feed/draft/](/feed/draft/) - static files like [/LICENSE](/LICENSE) - html anchors for headers like [#second-header](/your-first-article/#second-header) +- show random posts at the end of the page Everything is **[open-source](https://git.uphillsecurity.com/cf7/picopaper)**! diff --git a/picopaper.py b/picopaper.py index 6b5f389..700bc67 100644 --- a/picopaper.py +++ b/picopaper.py @@ -6,7 +6,7 @@ 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 +from config import BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_FEEDS_FROM_MAIN, NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH class SSGGGenerator: def __init__(self, items_dir='items', output_dir='output', theme=None, blog_title=None, blog_description=None): @@ -20,10 +20,21 @@ class SSGGGenerator: self.blog_description = blog_description or BLOG_DESCRIPTION self.exclude_feeds = EXCLUDE_FEEDS_FROM_MAIN self.navbar_items = NAVBAR_ITEMS + self.hide_logo = HIDE_LOGO + self.hide_title = HIDE_TITLE + self.logo_path = LOGO_PATH # Setup Jinja2 self.env = Environment(loader=FileSystemLoader(self.templates_dir)) + # Add custom filter for random sampling + def random_sample(items, count): + import random + items_list = list(items) + return random.sample(items_list, min(count, len(items_list))) + + self.env.filters['random_sample'] = random_sample + # Setup markdown with toc extension for header anchors self.md = markdown.Markdown(extensions=['extra', 'toc']) @@ -106,7 +117,7 @@ class SSGGGenerator: 'title': title, 'content': content, 'slug': parsed['name'], - 'url': f"{parsed['name']}/", + 'url': f"/{parsed['name']}/", 'feed': parsed['feed'], 'source': filepath.name } @@ -118,7 +129,7 @@ class SSGGGenerator: return posts - def generate_index(self, posts, feed_name=None): + def generate_index(self, posts, feed_name=None, all_posts=None): """Generate index.html with all posts (or feed-specific index)""" template = self.env.get_template('index.tmpl') @@ -134,7 +145,11 @@ class SSGGGenerator: blog_title=self.blog_title, blog_description=self.blog_description, navbar_items=self.navbar_items, - posts=posts + posts=posts, + all_posts=all_posts or posts, + hide_logo=self.hide_logo, + hide_title=self.hide_title, + logo_path=self.logo_path ) output_path.parent.mkdir(parents=True, exist_ok=True) @@ -143,7 +158,41 @@ class SSGGGenerator: print(f"✓ Generated {output_path}") - def generate_post_page(self, post): + def generate_feeds_overview(self, feeds, all_posts=None): + """Generate /feed/index.html with list of all non-excluded feeds""" + template = self.env.get_template('feeds.tmpl') + + # Prepare feed data with counts, excluding feeds in EXCLUDE_FEEDS_FROM_MAIN + feed_list = [] + for feed_name, posts in sorted(feeds.items()): + if feed_name not in self.exclude_feeds: + feed_list.append({ + 'name': feed_name, + 'count': len(posts) + }) + + title = f"Feeds - {self.blog_title}" + output_path = self.output_dir / 'feed' / 'index.html' + + html = template.render( + title=title, + blog_title=self.blog_title, + blog_description=self.blog_description, + navbar_items=self.navbar_items, + feeds=feed_list, + all_posts=all_posts or [], + hide_logo=self.hide_logo, + hide_title=self.hide_title, + logo_path=self.logo_path + ) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + + print(f"✓ Generated {output_path}") + + def generate_post_page(self, post, all_posts=None): """Generate individual post page for 'long' posts""" template = self.env.get_template('post.tmpl') @@ -152,7 +201,11 @@ class SSGGGenerator: blog_title=self.blog_title, blog_description=self.blog_description, navbar_items=self.navbar_items, - post=post + post=post, + all_posts=all_posts or [], + hide_logo=self.hide_logo, + hide_title=self.hide_title, + logo_path=self.logo_path ) # Create directory for the post slug @@ -216,7 +269,7 @@ class SSGGGenerator: and p['feed'] not in self.exclude_feeds] # Generate main index with filtered feed posts - self.generate_index(feed_posts) + self.generate_index(feed_posts, all_posts=feed_posts) # Group posts by feed (include all posts, not just those in main feed) feeds = {} @@ -226,12 +279,16 @@ class SSGGGenerator: # Generate feed-specific pages for feed_name, posts in feeds.items(): - self.generate_index(posts, feed_name) + self.generate_index(posts, feed_name, all_posts=feed_posts) + + # Generate feeds overview page + if feeds: + self.generate_feeds_overview(feeds, all_posts=feed_posts) # Generate individual pages for long posts, short posts, and pages for post in all_posts: if post['type'] in ['long', 'short', 'page']: - self.generate_post_page(post) + self.generate_post_page(post, all_posts=feed_posts) # Copy assets self.copy_assets() diff --git a/theme/default/assets/style.css b/theme/default/assets/style.css index 1663aa4..9b276fc 100644 --- a/theme/default/assets/style.css +++ b/theme/default/assets/style.css @@ -12,6 +12,7 @@ header { padding: 20px; margin-bottom: 40px; border-radius: 10px; + text-align: center; } img { @@ -22,7 +23,13 @@ hr { border: 1px solid #efefef; } -h1 { margin: 0; } +h1 { + margin: 0; +} + +.header-logo { + vertical-align: middle; +} .blog-description { margin: 10px 0 0 0; @@ -33,6 +40,10 @@ h1 { margin: 0; } .main-nav { margin-top: 15px; font-weight: bold; + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; } .nav-item { diff --git a/theme/default/templates/feeds.tmpl b/theme/default/templates/feeds.tmpl new file mode 100644 index 0000000..c03aae7 --- /dev/null +++ b/theme/default/templates/feeds.tmpl @@ -0,0 +1,20 @@ + + +
+ {% include 'meta.tmpl' %} + + + {% include 'header.tmpl' %} + +{{ blog_description }}