Render blog posts from Forgejo

This commit is contained in:
kacper 2026-04-12 20:23:05 -04:00
parent 6671a01d26
commit 0a9b052beb
6 changed files with 366 additions and 48 deletions

View file

@ -83,6 +83,11 @@ async def build_live_prototype_payload(
content_repos = [summary for summary in repo_summaries if summary is not None]
course_repos = [summary for summary in content_repos if summary["lesson_count"] > 0]
post_repos = [summary for summary in content_repos if summary["blog_count"] > 0]
blog_posts = sorted(
[post for summary in post_repos for post in summary["blog_posts"]],
key=lambda post: str(post.get("updated_at", "")),
reverse=True,
)
public_issues = await _recent_public_issues(
client,
public_repos,
@ -128,7 +133,7 @@ async def build_live_prototype_payload(
),
"source_of_truth": source_cards,
"featured_courses": [_course_card(summary) for summary in course_repos[:6]],
"recent_posts": [_post_card(summary) for summary in post_repos[:6]],
"recent_posts": [_post_card(post) for post in blog_posts[:6]],
"upcoming_events": _event_cards(calendar_feeds, settings.calendar_event_limit),
"recent_discussions": await asyncio.gather(
*[_discussion_card(client, issue) for issue in public_issues],
@ -232,9 +237,25 @@ async def _summarize_repo(
)
blog_count = 0
blog_posts: list[dict[str, object]] = []
if has_blogs:
blog_entries = await client.list_directory(owner_login, repo_name, "blogs")
blog_count = sum(1 for entry in blog_entries if entry.get("type") == "dir")
blog_dirs = _sorted_dir_entries(blog_entries)
blog_count = len(blog_dirs)
blog_posts = await asyncio.gather(
*[
_summarize_blog_post(
client,
owner_login,
repo_name,
str(repo.get("full_name", f"{owner_login}/{repo_name}")),
str(repo.get("description") or ""),
str(repo.get("updated_at", "")),
str(blog_dir.get("name", "")),
)
for blog_dir in blog_dirs
],
)
return {
"name": repo_name,
@ -245,6 +266,7 @@ async def _summarize_repo(
"lesson_count": lesson_count,
"chapter_count": chapter_count,
"blog_count": blog_count,
"blog_posts": blog_posts,
"updated_at": repo.get("updated_at", ""),
"course_outline": course_outline,
}
@ -266,15 +288,20 @@ def _course_card(summary: dict[str, Any]) -> dict[str, object]:
}
def _post_card(summary: dict[str, Any]) -> dict[str, object]:
post_count = int(summary["blog_count"])
label = "1 post folder detected" if post_count == 1 else f"{post_count} post folders detected"
def _post_card(post: dict[str, Any]) -> dict[str, object]:
return {
"title": summary["name"],
"repo": summary["full_name"],
"kind": "Repo with /blogs/",
"summary": f"{label}. {summary['description']}",
"updated_at": summary["updated_at"],
"title": post["title"],
"owner": post["owner"],
"name": post["name"],
"repo": post["repo"],
"slug": post["slug"],
"kind": "Blog post",
"summary": post["summary"],
"path": post["path"],
"file_path": post["file_path"],
"html_url": post["html_url"],
"body": post["body"],
"updated_at": post["updated_at"],
}
@ -474,6 +501,80 @@ def _auth_payload(
}
async def _summarize_blog_post(
client: ForgejoClient,
owner: str,
repo: str,
full_name: str,
repo_description: str,
updated_at: str,
post_name: str,
) -> dict[str, object]:
post_path = f"blogs/{post_name}"
fallback_title = _display_name(post_name)
try:
post_entries = await client.list_directory(owner, repo, post_path)
except ForgejoClientError:
return _empty_blog_post(
owner,
repo,
full_name,
post_name,
fallback_title,
repo_description,
updated_at,
post_path,
)
markdown_files = _markdown_file_entries(post_entries)
if not markdown_files:
return _empty_blog_post(
owner,
repo,
full_name,
post_name,
fallback_title,
repo_description,
updated_at,
post_path,
)
markdown_name = str(markdown_files[0]["name"])
markdown_path = f"{post_path}/{markdown_name}"
try:
file_payload = await client.get_file_content(owner, repo, markdown_path)
except ForgejoClientError:
return _empty_blog_post(
owner,
repo,
full_name,
post_name,
fallback_title,
repo_description,
updated_at,
post_path,
file_path=markdown_path,
html_url=str(markdown_files[0].get("html_url", "")),
)
metadata, body = _parse_frontmatter(str(file_payload.get("content", "")))
return {
"slug": post_name,
"title": str(metadata.get("title") or _display_name(markdown_name) or fallback_title),
"owner": owner,
"name": repo,
"repo": full_name,
"summary": str(metadata.get("summary") or repo_description or ""),
"path": post_path,
"file_path": str(file_payload.get("path", markdown_path)),
"html_url": str(file_payload.get("html_url", "")),
"body": body,
"updated_at": updated_at,
}
async def _summarize_lesson(
client: ForgejoClient,
owner: str,
@ -489,16 +590,7 @@ async def _summarize_lesson(
except ForgejoClientError:
return _empty_lesson(lesson_name, fallback_title, lesson_path)
markdown_files = sorted(
[
entry
for entry in lesson_entries
if entry.get("type") == "file"
and isinstance(entry.get("name"), str)
and str(entry.get("name", "")).lower().endswith(".md")
],
key=lambda entry: str(entry["name"]),
)
markdown_files = _markdown_file_entries(lesson_entries)
if not markdown_files:
return _empty_lesson(lesson_name, fallback_title, lesson_path)
@ -539,6 +631,19 @@ def _sorted_dir_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
)
def _markdown_file_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
return sorted(
[
entry
for entry in entries
if entry.get("type") == "file"
and isinstance(entry.get("name"), str)
and str(entry.get("name", "")).lower().endswith(".md")
],
key=lambda entry: str(entry["name"]),
)
def _display_name(value: str) -> str:
cleaned = value.strip().rsplit(".", 1)[0]
cleaned = cleaned.replace("_", " ").replace("-", " ")
@ -598,6 +703,34 @@ def _empty_lesson(
}
def _empty_blog_post(
owner: str,
repo: str,
full_name: str,
post_name: str,
title: str,
summary: str,
updated_at: str,
post_path: str,
*,
file_path: str = "",
html_url: str = "",
) -> dict[str, object]:
return {
"slug": post_name,
"title": title,
"owner": owner,
"name": repo,
"repo": full_name,
"summary": summary,
"path": post_path,
"file_path": file_path,
"html_url": html_url,
"body": "",
"updated_at": updated_at,
}
def _parse_frontmatter(markdown: str) -> tuple[dict[str, str], str]:
if not markdown.startswith("---\n"):
return {}, markdown.strip()