;;; recently-updated.el --- Generate recently-updated.org -*- lexical-binding: t; -*- ;;; Commentary: ;; Generates home/recently-updated.org listing the N most recently touched ;; Org files in the site tree. ;; ;; Sort order: #+DATE if parseable, otherwise file modification time. ;; This means intentionally dated pages float correctly, while undated ;; pages fall back to when they were last touched on disk. ;; ;; NOTE: Do NOT define `site-root' here. build-site.el owns that definition. ;;; Code: (require 'cl-lib) (require 'org) ;; ── Omit predicate ──────────────────────────────────────────────────────────── (defconst z/recent-omit-names '("sitemap.org" "categories.org" "recently-updated.org" "wip.org") "Org files that are always excluded from the recently-updated list.") (defun z/recent-omit-org-p (rel-org) "Return non-nil if REL-ORG (relative to site root) should be excluded." (let ((name (file-name-nondirectory rel-org)) (full (expand-file-name rel-org z/site-root))) (or ;; Static asset directories (string-prefix-p "assets/" rel-org) (string-match-p "/assets/" rel-org) ;; Tag pages (string-prefix-p "tags/" rel-org) (string-match-p "/tags/" rel-org) ;; Known generated files (member name z/recent-omit-names) ;; Deliberately discoverable only by hidden interactions. (z/hidden-page-p full) ;; Any *-list.org sitemaps (string-match-p "-list\\.org\\'" name)))) ;; ── Metadata reader ─────────────────────────────────────────────────────────── (defun z/read-org-meta (file) "Return (TITLE . TIME) for Org FILE. TITLE: from #+TITLE, or file-name-base when absent. TIME : from #+DATE (parsed as an Org time string) when present and parseable; otherwise falls back to the file's modification time. Using #+DATE ensures intentionally-dated content sorts by its canonical publication date rather than an accidental disk mtime." (with-temp-buffer (insert-file-contents file) (org-mode) (let* ((props (org-collect-keywords '("TITLE" "DATE"))) (title (or (cadr (assoc "TITLE" props)) (file-name-base file))) (date-str (cadr (assoc "DATE" props))) (time (or (and date-str (ignore-errors (org-time-string-to-time date-str))) (file-attribute-modification-time (file-attributes file))))) (cons title time)))) ;; ── Generator ──────────────────────────────────────────────────────────────── (defun z/generate-recently-updated-org (&optional n) "Write home/recently-updated.org listing the top N recently updated files. N defaults to 30. Returns the number of entries written." (let* ((count (or n 30)) (org-root (file-name-as-directory z/site-root)) (all-files (directory-files-recursively org-root "\\.org\\'")) (items '())) ;; Collect (REL TITLE TIME) (dolist (full all-files) (let ((rel (file-relative-name full org-root))) (unless (z/recent-omit-org-p rel) (pcase-let ((`(,title . ,time) (z/read-org-meta full))) (push (list rel title time) items))))) ;; Sort newest first (setq items (sort items (lambda (a b) (time-less-p (nth 2 b) (nth 2 a))))) ;; Trim to N (setq items (cl-subseq items 0 (min count (length items)))) ;; Write Org (with-temp-file (site-path "recently-updated.org") (insert "#+TITLE: Recently Updated\n" "#+OPTIONS: toc:nil num:nil\n\n" (format "* Recently Updated (top %d files)\n" (length items))) (dolist (it items) (let* ((rel (nth 0 it)) (title (nth 1 it)) (time (nth 2 it)) (datestr (format-time-string "%Y-%m-%d %H:%M" time))) (insert (format "- [[file:%s][%s]] @@html:%s@@\n" rel title datestr))))) (length items))) (provide 'recently-updated) ;;; recently-updated.el ends here