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 datetime
|
||||
import shutil
|
||||
import math
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
from socketserver import TCPServer
|
||||
from threading import Thread
|
||||
|
||||
import yaml
|
||||
import markdown2
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
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.")
|
||||
|
||||
|
||||
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"):
|
||||
# Common context
|
||||
kwargs = {
|
||||
|
@ -138,6 +226,9 @@ def generate_static_site(development_mode=False, theme="plain"):
|
|||
f"{template_name}.html", f"{template_name}.html", **context
|
||||
)
|
||||
|
||||
# Generate blog section
|
||||
generate_blog_html()
|
||||
|
||||
# Generate metrics
|
||||
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." />
|
||||
<meta name="twitter:image"
|
||||
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>
|
||||
{% block title %}
|
||||
{% endblock 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"
|
||||
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>
|
||||
<body>
|
||||
<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