Cybrkyd's git repositories

page-with-py • commit: 42111f7

commit 42111f784c57a53705e1f04a625743a382ad3ec1d30d7b2d652e891a300eb492
author cybrkyd <vii@cybrkyd.com> 2025-09-24 09:50:14 +0100
committer cybrkyd <vii@cybrkyd.com> 2025-09-24 09:50:14 +0100
v1.1

Commit Message

- v1.1
- New optional archive.html template
- Group posts by year if required from archive
- Navigation arrows change
- Pagination names changed to Newer and Older
- RSS now has Atom namespace

📊 Diffstat

pg-py/pa.py 101
1 files changed, 93 insertions(+), 8 deletions(-)

Diff

diff --git a/pg-py/pa.py b/pg-py/pa.py
index 446cf80..a8960b9 100644
--- a/pg-py/pa.py
+++ b/pg-py/pa.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
PaPy (Page with Python): a Python static site generator
- v1.0
+ v1.1
"""
import re
@@ -21,6 +21,8 @@ CONTENT_LANGUAGE = "en"
SITE_LOGO = "/img/logo.png"
THEME = "basic"
PAGINATION = "numbers" # "numbers" or "direction"
+ GENERATE_ARCHIVE = "yes" # "yes" or "no"
+ ARCHIVE_URL = "archive" # the URL of the archive
# Global caches
CSS_VAR_PATTERN = re.compile(r'(var\(--[^)]+\))')
@@ -86,6 +88,7 @@ def load_template_parts(theme_dir: Path):
single_file = theme_dir / 'single.html'
tags_file = theme_dir / 'tags.html'
page_file = theme_dir / 'page.html'
+ archive_file = theme_dir / 'archive.html'
if header_file.exists():
templates['header'] = header_file.read_text(encoding="utf-8")
@@ -123,6 +126,12 @@ def load_template_parts(theme_dir: Path):
templates['page'] = templates['single']
print("Warning: page.html not found in theme directory, using single.html")
+ if archive_file.exists():
+ templates['archive'] = archive_file.read_text(encoding="utf-8")
+ else:
+ templates['archive'] = templates['main']
+ print("Warning: archive.html not found in theme directory, using main.html")
+
if tags_file.exists():
templates['tags'] = tags_file.read_text(encoding="utf-8")
else:
@@ -161,6 +170,26 @@ def render_template(template_content, context):
for key, value in context.items():
if isinstance(value, str):
content = content.replace(f'{{{key}}}', value)
+
+ # Handle simple year grouping for archive template
+ if 'years' in context and '{% for year, posts in years %}' in content:
+ start_pattern = '{% for year, posts in years %}'
+ end_pattern = '{% endfor %}'
+ start_idx = content.find(start_pattern)
+ end_idx = content.find(end_pattern) + len(end_pattern)
+
+ if start_idx != -1 and end_idx != -1:
+ template_block = content[start_idx:end_idx]
+ content = content.replace(template_block, '')
+
+ years_content = ''
+ for year, posts in context['years'].items():
+ years_content += f'<h2>{year}</h2>\n'
+ for post in posts:
+ date_str = apply_filter(post.get('date', ''), 'format', '%d %b %Y')
+ years_content += f'<li>{date_str} - <a href="{post.get("url", "")}">{post.get("title", "")}</a></li>\n'
+
+ content = content.replace('</ul>', f'{years_content}</ul>')
content = process_filters(content, context)
@@ -336,7 +365,7 @@ def generate_post_navigation(current_post, all_posts):
'type': 'previous',
'url': prev_post['url_path'],
'title': prev_post['title'],
- 'label': '← Previous Post'
+ 'label': '&#8617; Previous Post'
})
# Next post (older)
@@ -346,7 +375,7 @@ def generate_post_navigation(current_post, all_posts):
'type': 'next',
'url': next_post['url_path'],
'title': next_post['title'],
- 'label': 'Next Post →'
+ 'label': 'Next Post &#8618;'
})
return navigation_items
@@ -373,7 +402,7 @@ def generate_index_page(templates, posts, output_dir: Path, page_num=1, is_pagin
prev_url = f"/tags/{tag_name}/page/{page_num-1}/" if page_num > 2 else f"/tags/{tag_name}/"
else:
prev_url = f"/page/{page_num-1}/" if page_num > 2 else "/"
- pagination_html += f'<span class="previous"><a href="{prev_url}">Previous</a></span> '
+ pagination_html += f'<span class="newer"><a href="{prev_url}">&#8617; Newer</a></span> '
if PAGINATION == "numbers":
for i in range(1, total_pages + 1):
@@ -388,9 +417,9 @@ def generate_index_page(templates, posts, output_dir: Path, page_num=1, is_pagin
if page_num < total_pages:
if tag_name:
- pagination_html += f'<a href="/tags/{tag_name}/page/{page_num+1}/">Next</a>'
+ pagination_html += f'<a href="/tags/{tag_name}/page/{page_num+1}/">Next Post &#8618;</a>'
else:
- pagination_html += f'<span class="next"><a href="/page/{page_num+1}/">Next</a></span>'
+ pagination_html += f'<span class="older"><a href="/page/{page_num+1}/">Older &#8618;</a></span>'
pagination_html += '</div>'
description = "Blog post archive" if is_pagination else SITE_DESC
@@ -441,6 +470,47 @@ def generate_index_page(templates, posts, output_dir: Path, page_num=1, is_pagin
return output_path
+ def group_posts_by_year(posts):
+ years = {}
+ for post in posts:
+ year = post['date'][:4]
+ if year not in years:
+ years[year] = []
+ years[year].append(post)
+
+ return dict(sorted(years.items(), key=lambda x: x[0], reverse=True))
+
+
+ def generate_archive_page(templates, posts, output_dir: Path):
+ title = f"Archive - {SITE_TITLE}"
+ description = f"Complete post archive - {SITE_DESC}"
+
+ # Fallback to main if archive not available
+ archive_template = templates.get('archive', templates['main'])
+
+ context = {
+ 'title': title,
+ 'description': description,
+ 'pagination': '',
+ 'years': group_posts_by_year(posts),
+ 'posts': [{
+ 'title': post['title'],
+ 'url': '/' + post['url'].replace(SITE_URL, '').lstrip('/'),
+ 'date': post.get('date', ''),
+ 'description': post.get('description', ''),
+ 'content': post.get('html_content', '')
+ } for post in posts]
+ }
+
+ content = render_template(archive_template, context)
+ page_url = f"{SITE_URL}/{ARCHIVE_URL}/"
+ full_html = generate_html_page(templates, title, content, description, page_url, "", "website")
+
+ output_path = output_dir / ARCHIVE_URL / 'index.html'
+ write_html_file(output_path, full_html)
+ return output_path
+
+
def generate_tags_index(templates, all_tags, output_dir: Path):
lines = ['<h1>Tags</h1>', '<ul>']
for tag, count in sorted(all_tags.items()):
@@ -459,11 +529,16 @@ def generate_tags_index(templates, all_tags, output_dir: Path):
def generate_rss_feed(posts, output_dir: Path):
- rss = ET.Element('rss', version='2.0')
+ # Create the root RSS element with Atom namespace
+ rss = ET.Element('rss', version='2.0', attrib={'xmlns:atom': 'http://www.w3.org/2005/Atom'})
channel = ET.SubElement(rss, 'channel')
ET.SubElement(channel, 'title').text = SITE_TITLE
ET.SubElement(channel, 'link').text = SITE_URL
ET.SubElement(channel, 'description').text = SITE_DESC
+ atom_link = ET.SubElement(channel, 'atom:link')
+ atom_link.set('href', f'{SITE_URL}/index.xml')
+ atom_link.set('rel', 'self')
+ atom_link.set('type', 'application/rss+xml')
recent_posts = posts[:40]
for post in recent_posts:
@@ -485,7 +560,12 @@ def generate_rss_feed(posts, output_dir: Path):
tree = ET.ElementTree(rss)
ET.indent(tree, space=" ", level=0)
- tree.write(output_dir / 'index.xml', encoding='utf-8', xml_declaration=True)
+
+ xml_content = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>\n'
+ rss_content = ET.tostring(rss, encoding='unicode', method='xml')
+ xml_content += rss_content
+
+ (output_dir / 'index.xml').write_text(xml_content, encoding='utf-8')
def generate_sitemap(pages, output_dir: Path):
@@ -568,10 +648,15 @@ def main():
all_pages.append({'url': f"{SITE_URL}/", 'path': str(index_page), 'date': datetime.now().replace(microsecond=0).isoformat()})
total_pages = (len(post_pages) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE
+
for page_num in range(2, total_pages + 1):
page_path = generate_index_page(templates, post_pages, public_dir, page_num=page_num, is_pagination=True)
all_pages.append({'url': f"{SITE_URL}/page/{page_num}/", 'path': str(page_path), 'date': datetime.now().replace(microsecond=0).isoformat()})
+ if GENERATE_ARCHIVE.lower() == "yes":
+ archive_path = generate_archive_page(templates, post_pages, public_dir)
+ all_pages.append({'url': f"{SITE_URL}/{ARCHIVE_URL}/", 'path': str(archive_path), 'date': datetime.now().replace(microsecond=0).isoformat()})
+
if all_tags_set:
tags_index_path = generate_tags_index(templates, tag_counts, public_dir)
all_pages.append({'url': f"{SITE_URL}/tags/", 'path': str(tags_index_path), 'date': datetime.now().replace(microsecond=0).isoformat()})