Compare commits

..

No commits in common. "98ab0ab419ea26680c199047673efcf2738467e1" and "e31624d03a7073c300037a90e00e22262f749609" have entirely different histories.

5 changed files with 25 additions and 82 deletions

View file

@ -4,7 +4,7 @@
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: beta - expect many changes - Status: alpha - expect many changes
- [Issue Tracker](https://git.uphillsecurity.com/cf7/picopaper/issues) - [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/)
@ -17,19 +17,21 @@ Show cases:
## Features ## Features
**Available**: **Available**:
- simple use, easy to understand and modify - Simple use, easy to understand and modify
- config file for settings - config file for settings
- Themes - Themes
- long- and short form content - Long- and short form content
- pages - Pages
- static files - Static files
- 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 - list random posts at the bottom
- optional RSS feeds
**Ideas**: **Ideas**:
- RSS
- Dark mode
- logo
- custom error pages (404, etc) - custom error pages (404, etc)
**Not planned**: **Not planned**:

View file

@ -15,8 +15,7 @@ EXCLUDE_FEEDS_FROM_MAIN = ['draft','private'] # e.g., ['python', 'drafts']
NAVBAR_ITEMS = [ NAVBAR_ITEMS = [
{'text': 'Home', 'url': '/'}, {'text': 'Home', 'url': '/'},
{'text': 'Feeds', 'url': '/feed/'}, {'text': 'Feeds', 'url': '/feed/'},
{'text': 'About', 'url': '/about/'}, {'text': 'About', 'url': '/about/'}
{'text': 'RSS', 'url': '/rss.xml'}
] ]
# Logo settings # Logo settings

View file

@ -1,3 +0,0 @@
# Another Project
Just a placeholer.

View file

@ -41,13 +41,8 @@ class SSGGGenerator:
# 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'])
def parse_filename(self, filename, subpath=''): def parse_filename(self, filename):
"""Parse filename format: YYYY-MM-DD_type_name[_feed].md """Parse filename format: YYYY-MM-DD_type_name[_feed].md"""
Args:
filename: The markdown filename
subpath: Optional subdirectory path (e.g., 'notes' for items/notes/)
"""
pattern = r'(\d{4}-\d{2}-\d{2})_(short|long|page)_(.+?)(?:_([a-z0-9-]+))?\.md' pattern = r'(\d{4}-\d{2}-\d{2})_(short|long|page)_(.+?)(?:_([a-z0-9-]+))?\.md'
match = re.match(pattern, filename) match = re.match(pattern, filename)
@ -63,8 +58,7 @@ class SSGGGenerator:
'type': post_type, 'type': post_type,
'name': name, 'name': name,
'feed': feed, 'feed': feed,
'filename': filename, 'filename': filename
'subpath': subpath
} }
def add_header_anchors(self, html_content): def add_header_anchors(self, html_content):
@ -103,46 +97,32 @@ class SSGGGenerator:
return title, html_content return title, html_content
def collect_posts(self): def collect_posts(self):
"""Collect and parse all posts from items directory, including subdirectories""" """Collect and parse all posts from items directory"""
posts = [] posts = []
if not self.items_dir.exists(): if not self.items_dir.exists():
print(f"Warning: {self.items_dir} does not exist") print(f"Warning: {self.items_dir} does not exist")
return posts return posts
# Use rglob to recursively find all .md files for filepath in self.items_dir.glob('*.md'):
for filepath in self.items_dir.rglob('*.md'): parsed = self.parse_filename(filepath.name)
# Calculate subpath relative to items_dir
relative_path = filepath.relative_to(self.items_dir)
subpath = str(relative_path.parent) if relative_path.parent != Path('.') else ''
parsed = self.parse_filename(filepath.name, subpath)
if not parsed: if not parsed:
print(f"Skipping {filepath}: doesn't match naming convention") print(f"Skipping {filepath.name}: doesn't match naming convention")
continue continue
title, content = self.read_post(filepath) title, content = self.read_post(filepath)
# Build slug and URL with subpath
if parsed['subpath']:
slug = f"{parsed['subpath']}/{parsed['name']}"
url = f"/{parsed['subpath']}/{parsed['name']}/"
else:
slug = parsed['name']
url = f"/{parsed['name']}/"
post = { post = {
'date': parsed['date_str'], 'date': parsed['date_str'],
'type': parsed['type'], 'type': parsed['type'],
'name': parsed['name'], 'name': parsed['name'],
'title': title, 'title': title,
'content': content, 'content': content,
'slug': slug, 'slug': parsed['name'],
'url': url, 'url': f"/{parsed['name']}/",
'feed': parsed['feed'], 'feed': parsed['feed'],
'source': str(relative_path), 'source': filepath.name
'subpath': parsed['subpath']
} }
posts.append(post) posts.append(post)
@ -219,35 +199,6 @@ class SSGGGenerator:
print(f"✓ Generated {output_path}") print(f"✓ Generated {output_path}")
def generate_subdir_index(self, subpath, posts, all_posts=None):
"""Generate index page for a subdirectory (e.g., /projects/)"""
template = self.env.get_template('index.tmpl')
# Use the subpath as the title (capitalize first letter)
subpath_title = subpath.replace('/', ' / ').title()
title = f"{subpath_title} - {self.blog_title}"
output_path = self.output_dir / subpath / 'index.html'
html = template.render(
title=title,
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,
rss_feed_enabled=ENABLE_RSS_FEED,
rss_feed_path=RSS_FEED_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, 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')
@ -266,9 +217,9 @@ class SSGGGenerator:
rss_feed_path=RSS_FEED_PATH rss_feed_path=RSS_FEED_PATH
) )
# Create directory for the post slug (with parents for nested paths) # Create directory for the post slug
post_dir = self.output_dir / post['slug'] post_dir = self.output_dir / post['slug']
post_dir.mkdir(parents=True, exist_ok=True) post_dir.mkdir(exist_ok=True)
# Generate index.html inside the slug directory # Generate index.html inside the slug directory
output_path = post_dir / 'index.html' output_path = post_dir / 'index.html'
@ -419,16 +370,6 @@ class SSGGGenerator:
if feeds: if feeds:
self.generate_feeds_overview(feeds, all_posts=feed_posts) self.generate_feeds_overview(feeds, all_posts=feed_posts)
# Group posts by subdirectory
subdirs = {}
for post in all_posts:
if post['subpath']: # Only posts in subdirectories
subdirs.setdefault(post['subpath'], []).append(post)
# Generate subdirectory index pages (e.g., /projects/)
for subpath, subdir_posts in subdirs.items():
self.generate_subdir_index(subpath, subdir_posts, 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']:

View file

@ -11,7 +11,11 @@
<article class="post"> <article class="post">
<div class="post-meta">{{ post.date }}</div> <div class="post-meta">{{ post.date }}</div>
<h2 class="post-title"> <h2 class="post-title">
{% if post.type in ['long', 'short'] %}
<a href="{{ post.url }}">{{ post.title }}</a> <a href="{{ post.url }}">{{ post.title }}</a>
{% else %}
{{ post.title }}
{% endif %}
</h2> </h2>
{% if post.type == 'short' %} {% if post.type == 'short' %}
<div class="post-content"> <div class="post-content">