(defvar site-root (file-name-directory (or load-file-name buffer-file-name))) (defun site-path (path) (expand-file-name path site-root)) (defun z/posts-sitemap (title list) "sitemap that lists post links as bullet points with dates and tags." (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "* Posts:\n" (mapconcat (lambda (entry) (let* ((link (car entry)) ;; extract relative file name from the link (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "posts/"))) (date-str "no date") (tags-str "")) ;; Get publish date (let ((date (org-publish-find-date full-path org-publish-project-alist))) (when date (setq date-str (format-time-string "%d-%m-%Y %H:%M" date)))) ;; Get FILETAGS from file buffer (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let* ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: %s @@" tag tag)) (split-string tags ":" t) ;; <- Splits by ":" and removes empty strings " ")))))) ;; Final line output (format "- %s @@html:%s@@ %s" link date-str tags-str))) (cdr list) "\n"))) (defun z/blogs-sitemap (title list) "sitemap that lists blog links as bullet points with dates and tags." (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "* Blogs:\n" (mapconcat (lambda (entry) (let* ((link (car entry)) ;; extract relative file name from the link (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "blogs/"))) (date-str "no date") (tags-str "")) ;; Get publish date (let ((date (org-publish-find-date full-path org-publish-project-alist))) (when date (setq date-str (format-time-string "%d-%m-%Y %H:%M" date)))) ;; Get FILETAGS from file buffer (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let* ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: %s @@" tag tag)) (split-string tags ":" t) ;; <- Splits by ":" and removes empty strings " ")))))) ;; Final line output (format "- %s @@html:%s@@ %s" link date-str tags-str))) (cdr list) "\n"))) (defun z/blogs-grouped-sitemap (title list) "Sitemap grouped by year and month with dates and FILETAGS." (let ((data (make-hash-table :test 'equal))) ;; STEP 1: Collect entries into (year -> month -> entries) (dolist (entry (cdr list)) (when (consp entry) (let* ((link (car entry)) (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "blogs/"))) (date (when (file-exists-p full-path) (org-publish-find-date full-path org-publish-project-alist)))) (when date (let* ((year (format-time-string "%Y" date)) (month (format-time-string "%B %Y" date)) (date-str (format-time-string "%d-%m-%Y %H:%M" date)) (tags-str "")) ;; Get tags (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: \ %s @@" tag tag)) (split-string tags ":" t) " ")))))) ;; Insert into hash table (let ((year-table (or (gethash year data) (puthash year (make-hash-table :test 'equal) data)))) (let ((month-list (gethash month year-table))) (puthash month (cons (list link date-str tags-str date) month-list) year-table)))))))) ;; STEP 2: Render output (let ((output (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n"))) ;; Sort years descending (dolist (year (sort (hash-table-keys data) #'string>)) (setq output (concat output "* " year "\n")) (let ((year-table (gethash year data))) ;; Sort months by actual date (descending) (dolist (month (sort (hash-table-keys year-table) (lambda (a b) (time-less-p (date-to-time (concat "01 " b)) (date-to-time (concat "01 " a)))))) (setq output (concat output "\n** " month "\n")) ;; Sort entries by date descending (dolist (entry (sort (gethash month year-table) (lambda (a b) (time-less-p (nth 3 b) (nth 3 a))))) (let ((link (nth 0 entry)) (date-str (nth 1 entry)) (tags-str (nth 2 entry))) (setq output (concat output (format "- %s @@html:%s@@ %s\n" link date-str tags-str)))))))) output))) (defun z/books-sitemap (title list) "sitemap that lists books." (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "* Book Notes:\n" (mapconcat (lambda (entry) (let* ((link (car entry)) ;; extract relative file name from the link (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "books/"))) (date-str "no date") (tags-str "")) ;; Get publish date (let ((date (org-publish-find-date full-path org-publish-project-alist))) (when date (setq date-str (format-time-string "%d-%m-%Y %H:%M" date)))) ;; Get FILETAGS from file buffer (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let* ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: %s @@" tag tag)) (split-string tags ":" t) ;; <- Splits by ":" and removes empty strings " ")))))) ;; Final line output (format "- %s @@html:%s@@ %s" link date-str tags-str))) (cdr list) "\n"))) (defun z/2025-sitemap (title list) "Sitemap that lists 2025 posts grouped by month, with dates and FILETAGS." (let ((output (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "* 2025\n")) (current-month nil)) ;; `list` is what org-publish passes in; we skip its car (top-level title node) (dolist (entry (cdr list)) ;; In this setup each ENTRY is usually (LINK . OTHER-STUFF) (when (consp entry) (let* ((link (car entry)) ;; Extract relative filename from the [[file:...][...]] link (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "blogs/2025/"))) (date (when (file-exists-p full-path) (org-publish-find-date full-path org-publish-project-alist))) (date-str (if date (format-time-string "%d-%m-%Y %H:%M" date) "no date")) (month-str (if date (format-time-string "%B %Y" date) ; e.g. "August 2025" "No date")) (tags-str "")) ;; Collect FILETAGS from the file (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: \ %s @@" tag tag)) (split-string tags ":" t) " ")))))) ;; Insert month heading whenever month changes (unless (equal month-str current-month) (setq current-month month-str) (setq output (concat output "\n** " month-str "\n"))) ;; Insert the actual entry (setq output (concat output (format "- %s @@html:%s@@ %s\n" link date-str tags-str)))))) output)) (defun z/2026-sitemap (title list) "Sitemap that lists 2026 blogs grouped by month, with dates and FILETAGS." (let ((output (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "* 2026\n")) (current-month nil)) (dolist (entry (cdr list)) (when (consp entry) (let* ((link (car entry)) (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "blogs/2026/"))) (date (when (file-exists-p full-path) (org-publish-find-date full-path org-publish-project-alist))) (date-str (if date (format-time-string "%d-%m-%Y %H:%M" date) "no date")) (month-str (if date (format-time-string "%B %Y" date) "No date")) (tags-str "")) (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: \ %s @@" tag tag)) (split-string tags ":" t) " ")))))) (unless (equal month-str current-month) (setq current-month month-str) (setq output (concat output "\n** " month-str "\n"))) (setq output (concat output (format "- %s @@html:%s@@ %s\n" link date-str tags-str)))))) output)) (defun z/career-sitemap (title list) "Sitemap that lists careers posts grouped by month, with dates and FILETAGS." (let ((output (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil \n\n" "See the categories: @@html:Categories@@\n\n" "See the following page for more details: @@html:Career Intro@@\n" )) (current-month nil)) (dolist (entry (cdr list)) (when (consp entry) (let* ((link (car entry)) (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename (site-path "posts/career/"))) (date (when (file-exists-p full-path) (org-publish-find-date full-path org-publish-project-alist))) (date-str (if date (format-time-string "%d-%m-%Y %H:%M" date) "no date")) (month-str (if date (format-time-string "%B %Y" date) ; e.g. "August 2025" "No date")) (tags-str "")) (when (file-exists-p full-path) (with-temp-buffer (insert-file-contents full-path) (org-mode) (let ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS")))))) (when tags (setq tags-str (mapconcat (lambda (tag) (format "@@html: \ %s @@" tag tag)) (split-string tags ":" t) " ")))))) ;; Insert month heading whenever month changes (unless (equal month-str current-month) (setq current-month month-str) (setq output (concat output "\n** " month-str "\n"))) (setq output (concat output (format "- %s @@html:%s@@ %s\n" link date-str tags-str)))))) output)) (defun z/categories-sitemap (title _list) "Generate a categories page by scanning tags across org-posts and org-blogs." (let* ((expanded (org-publish-expand-projects org-publish-project-alist)) (posts (assoc "org-posts" expanded)) (blogs (assoc "org-blogs" expanded)) ;; DO NOT set :exclude to "" — it excludes everything (files (cl-remove-duplicates (append (and posts (org-publish-get-base-files posts)) (and blogs (org-publish-get-base-files blogs))) :test #'file-equal-p)) ;; optional: drop the generated sitemaps (files (cl-remove-if (lambda (f) (member (file-name-nondirectory f) '("posts-list.org" "blogs-list.org" "sitemap.org" "categories.org"))) files)) (counts (make-hash-table :test 'equal))) ;;(message "files I will scan: %S" files) (dolist (f files) (when (file-readable-p f) (with-temp-buffer (insert-file-contents f) (org-mode) (let* ((kw (org-collect-keywords '("FILETAGS" "TAGS"))) (raw (car (or (cdr (assoc "FILETAGS" kw)) (cdr (assoc "TAGS" kw)))))) (when raw (dolist (tag (split-string raw ":" t)) (puthash tag (1+ (gethash tag counts 0)) counts))))))) (let (tags) (maphash (lambda (k _) (push k tags)) counts) (setq tags (sort tags #'string-lessp)) (concat "#+TITLE: " title "\n#+OPTIONS: toc:nil num:nil title:nil\n\n* Categories (Includes both blogs and posts)\n" (if tags (mapconcat (lambda (tag) (format "- [[file:../tags/%s.org][@@html:%s@@]] (%d)" (z/tag-slug tag) tag (gethash tag counts)) ) tags "\n") "_No tags found yet._"))))) (defun z/wip-file-p (file) "Return non-nil if FILE should be treated as WIP. A file is WIP if: - It has a #+WIP: keyword with any non-empty value, or - Its FILETAGS contain :WIP:, or - Any top-level heading contains the string \"WIP\" (case-insensitive)." (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (org-mode) (let* ((case-fold-search t) (keywords (org-collect-keywords '("WIP" "FILETAGS"))) (wip (cadr (assoc "WIP" keywords))) (filetags (cadr (assoc "FILETAGS" keywords)))) (or (and wip (string-match-p "\\S-" wip)) (and filetags (string-match-p ":WIP:" (concat ":" filetags ":"))) (save-excursion (goto-char (point-min)) (re-search-forward "^\\*+ .*WIP.*" nil t))))))) (defun z/wip-sitemap (title list) "Sitemap that lists only entries whose source files are WIP." (let* ((items (delq nil (mapcar (lambda (entry) (when (consp entry) (let* ((link (car entry)) (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link) (match-string 1 link) link)) (full-path (expand-file-name filename site-root))) (when (z/wip-file-p full-path) (let* ((date (org-publish-find-date full-path org-publish-project-alist)) (date-str (if date (format-time-string "%d-%m-%Y %H:%M" date) ""))) (format "- %s @@html:%s@@" link date-str)))))) (cdr list))))) ;; skip sitemap root (concat "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil\n\n" "* Work in progress\n" (if items (mapconcat #'identity items "\n") "No items currently marked as WIP.\n")))) (provide 'sitemaps)