feat: Add blog generation functionality
All checks were successful
Build and Deploy Static Site / build (push) Successful in 2m20s

Introduces a static blog generation feature leveraging markdown
for content files and template rendering for HTML output.

Imports new dependencies for YAML parsing and markdown conversion.
Enhances site structure by copy-assets function to ensure non-markdown
contents like images are maintained.

Modifies templates for relative asset path resolution to allow
correct linking of stylesheets and images.

Helps in managing content workflow by auto-generating paginated
lists and individual post pages, improving content accessibility.
This commit is contained in:
Kumi 2024-11-27 09:37:39 +01:00
parent f4c4b4aece
commit c4f333e2a5
Signed by: kumi
GPG key ID: ECBCC9082395383F
7 changed files with 165 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,7 @@
---
title: Test Post
date: 2024-11-27 09:00:00
---
This is a test post.
You can embed images: ![Duck image](image.png)

91
main.py
View file

@ -3,11 +3,15 @@ import json
import pathlib import pathlib
import datetime import datetime
import shutil import shutil
import math
from http.server import SimpleHTTPRequestHandler from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer from socketserver import TCPServer
from threading import Thread from threading import Thread
import yaml
import markdown2
from argparse import ArgumentParser from argparse import ArgumentParser
from helpers.finances import ( from helpers.finances import (
@ -63,6 +67,90 @@ def render_template_to_file(template_name, output_name, **kwargs):
print(f"Template {template_name} not found.") print(f"Template {template_name} not found.")
def calculate_relative_path(depth):
return "../" * depth
def copy_assets(src_dir, dest_dir):
for item in src_dir.iterdir():
if item.is_dir():
# Recur for subdirectories
item_dest_dir = dest_dir / item.name
item_dest_dir.mkdir(parents=True, exist_ok=True)
copy_assets(item, item_dest_dir)
elif item.is_file() and item.suffix not in [".md"]:
shutil.copy(item, dest_dir)
def parse_markdown_file(filepath):
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Split the front matter and markdown content
parts = content.split("---", 2)
if len(parts) == 3:
_, fm_text, md_content = parts
front_matter = yaml.safe_load(fm_text)
else:
front_matter, md_content = {}, content
return front_matter, md_content
def generate_blog_html(posts_per_page=5):
blog_dir = pathlib.Path("blog")
blog_posts = []
for post_dir in blog_dir.iterdir():
if post_dir.is_dir():
md_path = post_dir / "index.md"
if md_path.exists():
front_matter, md_content = parse_markdown_file(md_path)
html_content = markdown2.markdown(md_content)
front_matter["content"] = html_content
front_matter["slug"] = post_dir.name
blog_posts.append(front_matter)
# Ensure the build directory structure exists
output_post_dir = output_dir / "blog" / post_dir.name
output_post_dir.mkdir(parents=True, exist_ok=True)
# Copy non-markdown assets
copy_assets(post_dir, output_post_dir)
# Sort posts by date, descending
blog_posts.sort(key=lambda x: x.get("date", ""), reverse=True)
# Calculate total pages
total_posts = len(blog_posts)
total_pages = math.ceil(total_posts / posts_per_page)
# Generate each index page
for page in range(total_pages):
start = page * posts_per_page
end = start + posts_per_page
paginated_posts = blog_posts[start:end]
context = {
"posts": paginated_posts,
"current_page": page + 1,
"total_pages": total_pages,
"relative_path": calculate_relative_path(1 if page == 0 else 2),
}
output_path = (
f"blog/index.html" if page == 0 else f"blog/page/{page + 1}/index.html"
)
render_template_to_file("blog/index.html", output_path, **context)
# Render each individual post
for post in blog_posts:
post_slug = post["slug"]
render_template_to_file(
"blog/post.html",
f"blog/{post_slug}/index.html",
**{**post, "relative_path": calculate_relative_path(2)},
)
print("Blog section generated successfully.")
def generate_static_site(development_mode=False, theme="plain"): def generate_static_site(development_mode=False, theme="plain"):
# Common context # Common context
kwargs = { kwargs = {
@ -138,6 +226,9 @@ def generate_static_site(development_mode=False, theme="plain"):
f"{template_name}.html", f"{template_name}.html", **context f"{template_name}.html", f"{template_name}.html", **context
) )
# Generate blog section
generate_blog_html()
# Generate metrics # Generate metrics
balances = get_transparency_data(finances, allow_current=True)["end_balance"] balances = get_transparency_data(finances, allow_current=True)["end_balance"]

View file

@ -1 +1,3 @@
jinja2 jinja2
markdown2[all]
pyyaml

View file

@ -27,15 +27,19 @@
content="Private.coffee is a privacy-focused non-profit association, dedicated to supporting privacy and digital sovereignty." /> content="Private.coffee is a privacy-focused non-profit association, dedicated to supporting privacy and digital sovereignty." />
<meta name="twitter:image" <meta name="twitter:image"
content="https://private.coffee/assets/img/logo-inv_grad.png" /> content="https://private.coffee/assets/img/logo-inv_grad.png" />
<link rel="icon" type="image/png" href="assets/img/logo-inv_grad.png" /> <link rel="icon"
type="image/png"
href="{{ relative_path }}assets/img/logo-inv_grad.png" />
<title> <title>
{% block title %} {% block title %}
{% endblock title %} {% endblock title %}
- Private.coffee</title> - Private.coffee</title>
<link rel="stylesheet" href="assets/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="assets/css/base.css?v={{ timestamp }}" />
<link rel="stylesheet" <link rel="stylesheet"
href="assets/css/theme/{{ theme }}.css?v={{ timestamp }}" /> href="{{ relative_path }}assets/dist/css/bootstrap.min.css" />
<link rel="stylesheet"
href="{{ relative_path }}assets/css/base.css?v={{ timestamp }}" />
<link rel="stylesheet"
href="{{ relative_path }}assets/css/theme/{{ theme }}.css?v={{ timestamp }}" />
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-md py-3 navbar-light" id="mainNav"> <nav class="navbar navbar-expand-md py-3 navbar-light" id="mainNav">

43
templates/blog/index.html Normal file
View file

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}
Blog
{% endblock title %}
{% block content %}
<div class="container my-5">
<h1>Blog</h1>
<ul class="list-unstyled">
{% for post in posts %}
<li>
<a href="/blog/{{ post.slug }}/index.html">
{% if post.title %}
{{ post.title }}
{% else %}
{{ post.slug }}
{% endif %}
</a>
<br>
<small>{{ post.date }}</small>
</li>
{% endfor %}
</ul>
<nav class="mt-4">
<ul class="pagination">
{% if current_page > 1 %}
<li class="page-item">
<a class="page-link" href="/blog/page/{{ current_page - 1 }}/">Previous</a>
</li>
{% endif %}
{% for i in range(1, total_pages + 1) %}
<li class="page-item {% if i == current_page %}active{% endif %}">
<a class="page-link" href="/blog/page/{{ i }}/">{{ i }}</a>
</li>
{% endfor %}
{% if current_page < total_pages %}
<li class="page-item">
<a class="page-link" href="/blog/page/{{ current_page + 1 }}/">Next</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endblock content %}

13
templates/blog/post.html Normal file
View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<div class="container my-5">
<h1>{{ title }}</h1>
<p>
<small>{{ date }}</small>
</p>
<div>{{ content|safe }}</div>
</div>
{% endblock content %}