aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <hello@cleberg.net>2025-06-03 20:03:44 -0500
committerChristian Cleberg <hello@cleberg.net>2025-06-03 20:04:06 -0500
commitc03fcb2da2961c40cbb6000a2db61b161816d0a8 (patch)
tree2b153e73916ca7722c6d77274f69f66a932ead29
parentdaaca95cfa933cb3654e9e5596453fe4947ea96c (diff)
downloadcleberg.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.py173
-rw-r--r--theme/templates/index.html42
2 files changed, 171 insertions, 44 deletions
diff --git a/build.py b/build.py
index 43c942d..e1b5cd8 100644
--- a/build.py
+++ b/build.py
@@ -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>