diff --git a/build.el b/build.el index 7c6414c..d20d311 100644 --- a/build.el +++ b/build.el @@ -13,15 +13,76 @@ "") +(defun blog-extract-title (file) + "Extract #+TITLE: value from FILE." + (with-temp-buffer + (insert-file-contents file) + (goto-char (point-min)) + (if (re-search-forward "^#\\+TITLE:[ \t]*\\(.+\\)$" nil t) + (string-trim (match-string 1)) + (file-name-base file)))) + +(defun blog-extract-excerpt (file &optional max-chars) + "Extract first body paragraph from FILE, up to MAX-CHARS characters." + (let ((max (or max-chars 250))) + (with-temp-buffer + (insert-file-contents file) + (goto-char (point-min)) + ;; Skip metadata lines and headings + (while (and (not (eobp)) + (looking-at "^\\(#\\+\\|\\*\\|[ \t]*$\\)")) + (forward-line 1)) + (let ((start (point))) + ;; Collect lines until blank line or heading + (while (and (not (eobp)) + (not (looking-at "^\\(\\*\\|[ \t]*$\\)"))) + (forward-line 1)) + (let ((text (string-trim (buffer-substring-no-properties start (point))))) + ;; Collapse whitespace + (setq text (replace-regexp-in-string "[ \t\n]+" " " text)) + (if (> (length text) max) + (concat (substring text 0 max) "...") + text)))))) + +(defun blog-generate-listings (_project) + "Scan content/blog/ and generate recent-posts.org and all-posts.org." + (let* ((blog-dir "./content/blog") + (gen-dir "./content/generated") + (pattern (concat blog-dir "/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*.org")) + (files (sort (file-expand-wildcards pattern t) + (lambda (a b) (string> (file-name-base a) + (file-name-base b))))) + (make-entry + (lambda (file) + (let* ((base (file-name-base file)) + (date (substring base 0 10)) + (title (blog-extract-title file)) + (excerpt (blog-extract-excerpt file)) + (link (concat "file:../blog/" base ".org"))) + (concat + "*[[" link "][" title "]]* — " date "\n\n" + excerpt "\n\n" + "[[" link "][Read more →]]\n\n" + "-----\n\n"))))) + (make-directory gen-dir t) + (with-temp-file (concat gen-dir "/recent-posts.org") + (dolist (file (seq-take files 3)) + (insert (funcall make-entry file)))) + (with-temp-file (concat gen-dir "/all-posts.org") + (dolist (file files) + (insert (funcall make-entry file)))))) (setq org-publish-project-alist `(("site-pages" :base-directory "./content" :base-extension "org" :publishing-directory "./public" :recursive t + :exclude "generated/" :publishing-function org-html-publish-to-html + :preparation-function blog-generate-listings :html-head-include-default-style nil :html-head-include-scripts nil :html-head "" diff --git a/content/blog.org b/content/blog.org new file mode 100644 index 0000000..8e700ca --- /dev/null +++ b/content/blog.org @@ -0,0 +1,6 @@ +#+TITLE: Blog +#+DESCRIPTION: All blog posts + +* All Posts + +#+INCLUDE: generated/all-posts.org diff --git a/content/blog/post001.org b/content/blog/2025-05-08-nix-selfhosting-in-the-age-of-llms.org similarity index 100% rename from content/blog/post001.org rename to content/blog/2025-05-08-nix-selfhosting-in-the-age-of-llms.org diff --git a/content/index.org b/content/index.org index a802664..81bad44 100644 --- a/content/index.org +++ b/content/index.org @@ -3,7 +3,7 @@ * Recent Blog Posts -- [[file:blog/post001.org][Nix Selfhosting in the Age of LLMs]] +#+INCLUDE: generated/recent-posts.org * Contact me diff --git a/flake.nix b/flake.nix index 244d8be..22b04bb 100644 --- a/flake.nix +++ b/flake.nix @@ -95,9 +95,37 @@ ''; }; + new-post = pkgs.writeShellApplication { + name = "new-post"; + runtimeInputs = [ pkgs.coreutils pkgs.gnused ]; + text = '' + if [ $# -eq 0 ]; then + echo "Usage: new-post \"Post Title\"" + exit 1 + fi + title="$*" + slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//;s/-$//') + date=$(date +%Y-%m-%d) + filename="content/blog/''${date}-''${slug}.org" + mkdir -p content/blog + cat > "$filename" <