diff options
author | Christian Cleberg <hello@cleberg.net> | 2025-06-03 20:03:44 -0500 |
---|---|---|
committer | Christian Cleberg <hello@cleberg.net> | 2025-06-03 20:04:06 -0500 |
commit | c03fcb2da2961c40cbb6000a2db61b161816d0a8 (patch) | |
tree | 2b153e73916ca7722c6d77274f69f66a932ead29 | |
parent | daaca95cfa933cb3654e9e5596453fe4947ea96c (diff) | |
download | cleberg.net-c03fcb2da2961c40cbb6000a2db61b161816d0a8.tar.gz cleberg.net-c03fcb2da2961c40cbb6000a2db61b161816d0a8.tar.bz2 cleberg.net-c03fcb2da2961c40cbb6000a2db61b161816d0a8.zip |
feat: upgrade build script to dynamically create latest posts section
-rw-r--r-- | build.py | 173 | ||||
-rw-r--r-- | theme/templates/index.html | 42 |
2 files changed, 171 insertions, 44 deletions
@@ -1,9 +1,174 @@ #!/usr/bin/env python3 import os +import re import shutil import subprocess import sys from pathlib import Path +from datetime import datetime + +def update_index_html(html_snippet, template_path="./theme/templates/index.html"): + """ + Read the index.html file at `template_path`, replace everything between + <!-- BEGIN_POSTS --> and <!-- END_POSTS --> with the provided html_snippet, + and write the updated content back to the same file. + """ + # Read the current contents of index.html + with open(template_path, "r", encoding="utf-8") as f: + content = f.read() + + begin_marker = "<!-- BEGIN_POSTS -->" + end_marker = "<!-- END_POSTS -->" + + # Find the indices of the markers + begin_index = content.find(begin_marker) + end_index = content.find(end_marker) + + if begin_index == -1 or end_index == -1: + raise ValueError(f"Markers not found in {template_path}") + + # Compute insertion points: after the end of begin_marker line, before end_marker + # Include the newline after BEGIN_POSTS + insert_start = begin_index + len(begin_marker) + # Ensure we capture the newline character if present + if content[insert_start:insert_start+1] == "\n": + insert_start += 1 + + # If there is a newline before END_POSTS, trim trailing whitespace from snippet block + # We will preserve indentation of BEGIN_POSTS line + indent = "" + # Determine the indentation by looking at characters after the newline that follows begin_marker + lines_after_begin = content[begin_index:].splitlines(True) + if len(lines_after_begin) > 1: + # The second line starts with the indentation to preserve + second_line = lines_after_begin[1] + indent = "" + for ch in second_line: + if ch.isspace(): + indent += ch + else: + break + + # Prepare the replacement block: indent each line of html_snippet + snippet_lines = html_snippet.splitlines() + indented_snippet = "\n".join(indent + line for line in snippet_lines) + "\n" + + # Compute the position just before end_marker (excluding any preceding whitespace/newline) + end_line_start = content.rfind("\n", 0, end_index) + if end_line_start == -1: + end_line_start = end_index + + # Construct the new content + new_content = ( + content[:insert_start] + + indented_snippet + + content[end_line_start:] + ) + + # Write back to index.html + with open(template_path, "w", encoding="utf-8") as f: + f.write(new_content) + +def get_recent_posts_html(content_dir="./content/blog", num_posts=3): + """ + Scan the `content_dir` for .org blog post files, extract their headers, + and return an HTML snippet for the `num_posts` most recent posts. + + Expects each .org file to contain headers of the form: + #+title: Post Title + #+date: <YYYY-MM-DD Day HH:MM:SS> + #+slug: post-slug + #+filetags: :tag1:tag2:tag3: + + Returns a string containing the section with the three most recent posts, + formatted like the snippet in the prompt. + """ + posts = [] + + header_patterns = { + "title": re.compile(r'^#\+title:\s*(.+)$', re.IGNORECASE), + "date": re.compile(r'^#\+date:\s*<(\d{4}-\d{2}-\d{2})'), + "slug": re.compile(r'^#\+slug:\s*(.+)$', re.IGNORECASE), + "filetags": re.compile(r'^#\+filetags:\s*(.+)$', re.IGNORECASE), + } + + for org_path in Path(content_dir).glob("*.org"): + title = None + date_str = None + slug = None + tags = [] + + with org_path.open("r", encoding="utf-8") as f: + for line in f: + if title is None: + m = header_patterns["title"].match(line) + if m: + title = m.group(1).strip() + continue + + if date_str is None: + m = header_patterns["date"].match(line) + if m: + # date_str is just YYYY-MM-DD + date_str = m.group(1) + continue + + if slug is None: + m = header_patterns["slug"].match(line) + if m: + slug = m.group(1).strip() + continue + + if not tags: + m = header_patterns["filetags"].match(line) + if m: + # filetags are in the form ":tag1:tag2:tag3:" + raw = m.group(1).strip() + # split on ":" and filter out empty strings + tags = [t for t in raw.split(":") if t] + continue + + # Stop scanning once we have all required fields + if title and date_str and slug and tags: + break + + if title and date_str and slug: + try: + date_obj = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + # Skip files with invalid date format + continue + + posts.append({ + "title": title, + "date_str": date_str, + "date_obj": date_obj, + "slug": slug, + "tags": tags, + }) + + # Sort posts by date (newest first) + posts.sort(key=lambda x: x["date_obj"], reverse=True) + + # Take the top `num_posts` + recent = posts[:num_posts] + + # Build HTML lines + lines = [] + for post in recent: + lines.append('\t<div class="post">') + lines.append(f'\t\t<time datetime="{post["date_str"]}">{post["date_str"]}</time>') + lines.append('\t\t<div class="post-content">') + lines.append(f'\t\t\t<a href="/blog/{post["slug"]}.html">{post["title"]}</a>') + if post["tags"]: + lines.append('\t\t\t<div class="post-tags">') + for tag in post["tags"]: + lines.append(f'\t\t\t\t<span class="tag">{tag}</span>') + lines.append('\t\t\t</div>') + lines.append('\t\t</div>') + lines.append('\t</div>') + + return "\n".join(lines) def prompt(prompt_text): @@ -87,16 +252,14 @@ def start_dev_server(build_dir): def main(): + html_snippet = get_recent_posts_html("./content/blog", num_posts=3) + update_index_html(html_snippet) + build_dir = Path(".build") theme_dir = Path("theme/static") css_src = theme_dir / "styles.css" css_min = theme_dir / "styles.min.css" - answer = prompt("Did you update the 'Recent Blog Posts' section? [yn] ") - if not answer.lower().startswith("y"): - print("Please update the 'Recent Blog Posts' section before publishing!") - sys.exit(0) - env = os.environ.get("ENV", "").lower() if env == "prod": print("Environment: Production") diff --git a/theme/templates/index.html b/theme/templates/index.html index 249ba02..cbd91c3 100644 --- a/theme/templates/index.html +++ b/theme/templates/index.html @@ -8,45 +8,9 @@ sub rsa4096 2022-11-16 [E]</pre> </section> <section> <h2>Recent Blog Posts</h2> - - - <div class="post"> - <time datetime="2025-06-03 15:26:05">2025-06-02</time> - <div class="post-content"> - <a href="/blog/private-ios-apps.html">Selection of Privacy-Focused iOS Applications for Minimalist Users</a> - <div class="post-tags"> - <span class="tag">ios</span> - <span class="tag">privacy</span> - <span class="tag">security</span> - </div> - </div> - </div> - - <div class="post"> - <time datetime="2025-05-30 10:53:28">2025-05-30</time> - <div class="post-content"> - <a href="/blog/it-audit-career.html">Career Advancement through Mastery of Emerging Technologies in IT Audit</a> - <div class="post-tags"> - <span class="tag">audit</span> - <span class="tag">technology</span> - <span class="tag">career</span> - </div> - </div> - </div> - - <div class="post"> - <time datetime="2025-05-02 Fri 21:10:00">2025-05-02</time> - <div class="post-content"> - <a href="/blog/asahi-linux.html">Performance and Compatibility Report of Asahi Linux on Apple M2 MacBook Pro 16"</a> - <div class="post-tags"> - <span class="tag">mac</span> - <span class="tag">apple</span> - <span class="tag">linux</span> - </div> - </div> - </div> - - <br /> + <!-- BEGIN_POSTS --> + <!-- END_POSTS --> + <br> <a href="/blog/">All Posts →</a> </section> |