Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

10 changed files with 12 additions and 151 deletions

View file

@ -1,11 +1,8 @@
![picopaper logo](images/logo.png)
# 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/)
@ -26,7 +23,6 @@ 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
@ -161,12 +157,6 @@ 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).

View file

@ -1,6 +1,6 @@
"""Configuration file for picopaper blog"""
BLOG_TITLE = "PicoPaper.com"
BLOG_TITLE = "picopaper"
BLOG_DESCRIPTION = "we like simple."
THEME = "default"
@ -10,12 +10,6 @@ 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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -7,7 +7,6 @@
- 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)**!

View file

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

View file

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

View file

@ -1,20 +0,0 @@
<!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,14 +1,5 @@
<header>
<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>
<h1><a href="/" style="color: #333; text-decoration: none;">{{ blog_title }}</a></h1>
<p class="blog-description">{{ blog_description }}</p>
<nav class="main-nav">
{% for item in navbar_items %}

View file

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

View file

@ -1,23 +0,0 @@
{% 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 %}