Compare commits

..

9 commits
v0.2.0 ... main

10 changed files with 151 additions and 12 deletions

View file

@ -1,8 +1,11 @@
![picopaper logo](images/logo.png)
# picopaper # picopaper
A minimal static site generator for blogs built with Python 3 and Jinja2 A minimal static site generator for blogs built with Python 3 and Jinja2
- Status: alpha - expect many changes - Status: alpha - expect many changes
- [Issue Tracker](https://git.uphillsecurity.com/cf7/picopaper/issues)
- Goals: keeping it simple and easy to understand - Goals: keeping it simple and easy to understand
- Demo: [picopaper.com](https://picopaper.com/) - Demo: [picopaper.com](https://picopaper.com/)
@ -23,6 +26,7 @@ Show cases:
- separate feeds (used for categories, tagging, etc) `/feed/{tag}` - separate feeds (used for categories, tagging, etc) `/feed/{tag}`
- exclusion of feeds from main feed (drafts or system notes) - exclusion of feeds from main feed (drafts or system notes)
- HTML anchors for headers - HTML anchors for headers
- list random posts at the bottom
**Ideas**: **Ideas**:
- RSS - RSS
@ -157,6 +161,12 @@ To switch themes, change the `THEME` setting in `config.py`
--- ---
## Notes
- [Github Mirror](https://github.com/CaffeineFueled1/picopaper)
---
## Security ## Security
For security concerns or reports, please contact via `hello a t uphillsecurity d o t com` [gpg](https://uphillsecurity.com/gpg). For security concerns or reports, please contact via `hello a t uphillsecurity d o t com` [gpg](https://uphillsecurity.com/gpg).

View file

@ -1,6 +1,6 @@
"""Configuration file for picopaper blog""" """Configuration file for picopaper blog"""
BLOG_TITLE = "picopaper" BLOG_TITLE = "PicoPaper.com"
BLOG_DESCRIPTION = "we like simple." BLOG_DESCRIPTION = "we like simple."
THEME = "default" 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 # Navigation bar items - list of dictionaries with 'text' and 'url' keys
NAVBAR_ITEMS = [ NAVBAR_ITEMS = [
{'text': 'Home', 'url': '/'}, {'text': 'Home', 'url': '/'},
{'text': 'Feeds', 'url': '/feed/'},
{'text': 'About', 'url': '/about/'} {'text': 'About', 'url': '/about/'}
] ]
# Logo settings
HIDE_LOGO = False
HIDE_TITLE = True
LOGO_PATH = "/images/logo.png"

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -7,6 +7,7 @@
- excluding feeds in config file like [/feed/draft/](/feed/draft/) - excluding feeds in config file like [/feed/draft/](/feed/draft/)
- static files like [/LICENSE](/LICENSE) - static files like [/LICENSE](/LICENSE)
- html anchors for headers like [#second-header](/your-first-article/#second-header) - 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)**! Everything is **[open-source](https://git.uphillsecurity.com/cf7/picopaper)**!

View file

@ -6,7 +6,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import markdown 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: class SSGGGenerator:
def __init__(self, items_dir='items', output_dir='output', theme=None, blog_title=None, blog_description=None): 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.blog_description = blog_description or BLOG_DESCRIPTION
self.exclude_feeds = EXCLUDE_FEEDS_FROM_MAIN self.exclude_feeds = EXCLUDE_FEEDS_FROM_MAIN
self.navbar_items = NAVBAR_ITEMS self.navbar_items = NAVBAR_ITEMS
self.hide_logo = HIDE_LOGO
self.hide_title = HIDE_TITLE
self.logo_path = LOGO_PATH
# Setup Jinja2 # Setup Jinja2
self.env = Environment(loader=FileSystemLoader(self.templates_dir)) 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 # Setup markdown with toc extension for header anchors
self.md = markdown.Markdown(extensions=['extra', 'toc']) self.md = markdown.Markdown(extensions=['extra', 'toc'])
@ -106,7 +117,7 @@ class SSGGGenerator:
'title': title, 'title': title,
'content': content, 'content': content,
'slug': parsed['name'], 'slug': parsed['name'],
'url': f"{parsed['name']}/", 'url': f"/{parsed['name']}/",
'feed': parsed['feed'], 'feed': parsed['feed'],
'source': filepath.name 'source': filepath.name
} }
@ -118,7 +129,7 @@ class SSGGGenerator:
return posts 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)""" """Generate index.html with all posts (or feed-specific index)"""
template = self.env.get_template('index.tmpl') template = self.env.get_template('index.tmpl')
@ -134,7 +145,11 @@ class SSGGGenerator:
blog_title=self.blog_title, blog_title=self.blog_title,
blog_description=self.blog_description, blog_description=self.blog_description,
navbar_items=self.navbar_items, 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) output_path.parent.mkdir(parents=True, exist_ok=True)
@ -143,7 +158,41 @@ class SSGGGenerator:
print(f"✓ Generated {output_path}") 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""" """Generate individual post page for 'long' posts"""
template = self.env.get_template('post.tmpl') template = self.env.get_template('post.tmpl')
@ -152,7 +201,11 @@ class SSGGGenerator:
blog_title=self.blog_title, blog_title=self.blog_title,
blog_description=self.blog_description, blog_description=self.blog_description,
navbar_items=self.navbar_items, 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 # Create directory for the post slug
@ -216,7 +269,7 @@ class SSGGGenerator:
and p['feed'] not in self.exclude_feeds] and p['feed'] not in self.exclude_feeds]
# Generate main index with filtered feed posts # 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) # Group posts by feed (include all posts, not just those in main feed)
feeds = {} feeds = {}
@ -226,12 +279,16 @@ class SSGGGenerator:
# Generate feed-specific pages # Generate feed-specific pages
for feed_name, posts in feeds.items(): 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 # Generate individual pages for long posts, short posts, and pages
for post in all_posts: for post in all_posts:
if post['type'] in ['long', 'short', 'page']: if post['type'] in ['long', 'short', 'page']:
self.generate_post_page(post) self.generate_post_page(post, all_posts=feed_posts)
# Copy assets # Copy assets
self.copy_assets() self.copy_assets()

View file

@ -12,6 +12,7 @@ header {
padding: 20px; padding: 20px;
margin-bottom: 40px; margin-bottom: 40px;
border-radius: 10px; border-radius: 10px;
text-align: center;
} }
img { img {
@ -22,7 +23,13 @@ hr {
border: 1px solid #efefef; border: 1px solid #efefef;
} }
h1 { margin: 0; } h1 {
margin: 0;
}
.header-logo {
vertical-align: middle;
}
.blog-description { .blog-description {
margin: 10px 0 0 0; margin: 10px 0 0 0;
@ -33,6 +40,10 @@ h1 { margin: 0; }
.main-nav { .main-nav {
margin-top: 15px; margin-top: 15px;
font-weight: bold; font-weight: bold;
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
} }
.nav-item { .nav-item {

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include 'meta.tmpl' %}
</head>
<body>
{% include 'header.tmpl' %}
<main>
<h2>Feeds</h2>
<ul>
{% for feed in feeds %}
<li><a href="/feed/{{ feed.name }}/">{{ feed.name }}</a> ({{ feed.count }} posts)</li>
{% endfor %}
</ul>
</main>
{% include 'footer.tmpl' %}
</body>
</html>

View file

@ -1,5 +1,14 @@
<header> <header>
<h1><a href="/" style="color: #333; text-decoration: none;">{{ blog_title }}</a></h1> <h1>
<a href="/" style="color: #333; text-decoration: none;">
{% if not hide_logo %}
<img src="{{ logo_path }}" alt="{{ blog_title }}" class="header-logo">
{% endif %}
{% if not hide_title %}
{{ blog_title }}
{% endif %}
</a>
</h1>
<p class="blog-description">{{ blog_description }}</p> <p class="blog-description">{{ blog_description }}</p>
<nav class="main-nav"> <nav class="main-nav">
{% for item in navbar_items %} {% for item in navbar_items %}

View file

@ -14,6 +14,8 @@
{{ post.content | safe }} {{ post.content | safe }}
</div> </div>
</article> </article>
{% include 'random_posts.tmpl' %}
</main> </main>
{% include 'footer.tmpl' %} {% include 'footer.tmpl' %}

View file

@ -0,0 +1,23 @@
{% if all_posts %}
{# Filter out pages and current post, then randomly select 5 #}
{% set available_posts = all_posts | selectattr('type', 'ne', 'page') | list %}
{% if post %}
{% set available_posts = available_posts | rejectattr('slug', 'equalto', post.slug) | list %}
{% endif %}
{% if available_posts %}
{% set random_posts = available_posts | random_sample(5) %}
{% if random_posts %}
<div class="random-posts">
<h3>More Posts</h3>
<ul>
{% for p in random_posts %}
<li>
<a href="{{ p.url }}">{{ p.title }}</a>
<span class="date">{{ p.date }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
{% endif %}