feat: Add blog generation functionality
All checks were successful
Build and Deploy Static Site / build (push) Successful in 2m20s
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:
parent
f4c4b4aece
commit
c4f333e2a5
7 changed files with 165 additions and 5 deletions
BIN
blog/2024112701-test/image.png
Normal file
BIN
blog/2024112701-test/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
7
blog/2024112701-test/index.md
Normal file
7
blog/2024112701-test/index.md
Normal 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
91
main.py
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
jinja2
|
jinja2
|
||||||
|
markdown2[all]
|
||||||
|
pyyaml
|
|
@ -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
43
templates/blog/index.html
Normal 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
13
templates/blog/post.html
Normal 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 %}
|
Loading…
Reference in a new issue