;; ----------------------------- ;; Package bootstrap (BATCH SAFE) ;; ----------------------------- (require 'package) (setq package-user-dir (expand-file-name "./.packages")) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("gnu" . "https://elpa.gnu.org/packages/"))) (package-initialize) (unless package-archive-contents (package-refresh-contents)) (unless (package-installed-p 'org-roam) (package-install 'org-roam)) (require 'ox-publish) (require 'org) (require 'ox-html) (require 'json) (require 'org-roam) (require 'ob-latex) (require 'seq) (require 'cl-lib) (setq org-id-locations-file "~/.emacs.d/.org-id-locations") (defun z/org-html-wrap-content-for-stack (orig-fun contents info) "Wrap Org HTML #content in stack container, leaving postamble outside." (let* ((html (funcall orig-fun contents info)) (page (file-name-base (plist-get info :output-file))) (input-file (plist-get info :input-file)) ;; Extract planted date from filename (format: YYYYMMDDHHMMSS-title) (planted-date (if (string-match "^\\([0-9]\\{14\\}\\)" page) (let ((date-str (match-string 1 page))) (condition-case nil (format-time-string "%Y-%m-%d" (encode-time (string-to-number (substring date-str 12 14)) ; second (string-to-number (substring date-str 10 12)) ; minute (string-to-number (substring date-str 8 10)) ; hour (string-to-number (substring date-str 6 8)) ; day (string-to-number (substring date-str 4 6)) ; month (string-to-number (substring date-str 0 4)))) ; year (error "Unknown"))) ;; Fallback: try to get creation date from input file (let ((file-path (if input-file (expand-file-name input-file (plist-get info :base-directory)) nil))) (if (and file-path (file-exists-p file-path)) (format-time-string "%Y-%m-%d" (nth 4 (file-attributes file-path))) "Unknown")))) ;; Get last modified date (last tended to) (last-tended (let ((file-path (if input-file (expand-file-name input-file (plist-get info :base-directory)) nil))) (if (and file-path (file-exists-p file-path)) (format-time-string "%Y-%m-%d" (nth 5 (file-attributes file-path))) "Unknown"))) (stack-close "")) (setq html (replace-regexp-in-string "
" (format "
" page) html nil t)) ;; Wrap title with metadata section - use lambda to properly capture title ;; Handle titles with nested HTML tags by matching everything between opening and closing h1 tags ;; Use .* to match any characters (greedy) - will match up to the last which should be our closing tag (setq html (replace-regexp-in-string "

\\(.*\\)

" (lambda (match) (let ((title-text (match-string 1 match))) (format "

%s

" title-text planted-date last-tended))) html nil t)) (setq html (replace-regexp-in-string "
" (concat stack-close "\n
") html nil t)) html)) (advice-add 'org-html-template :around #'z/org-html-wrap-content-for-stack) (add-to-list 'load-path default-directory) (defvar z-shared-head " " ) (defvar z-preamble "
\"Site
Updated: %C
" ) (defvar z-postamble "") (defvar z/build-full-rebuild t "If non-nil, generate expensive artifacts like search-index.json.") (defun z/generate-all-files-page () "Generate all-files.org from search-index.json." (let* ((json-file "~/master-folder/org_files/org_roam/output/search-index.json") (out-file "~/master-folder/org_files/org_roam/all-files.org") (data (append (json-read-file json-file) nil))) (message "🔎 Loaded %d entries from search-index.json" (length data)) (with-temp-file out-file (insert "#+title: All Files\n") (insert "#+options: toc:nil\n\n") (insert "A complete alphabetical index of all published pages.\n\n") (setq data (sort data (lambda (a b) (string-lessp (downcase (cdr (assoc 'title a))) (downcase (cdr (assoc 'title b))))))) (let ((current-letter nil)) (dolist (entry data) (let* ((title (cdr (assoc 'title entry))) (raw-url (cdr (assoc 'url entry))) (id (cdr (assoc 'id entry))) (letter (upcase (substring title 0 1)))) (unless (equal letter current-letter) (setq current-letter letter) (insert (format "* %s\n" letter))) (insert (format "- [[id:%s][%s]]\n" id title)))))))) ;; Babel language support (org-babel-do-load-languages 'org-babel-load-languages '((latex . t) (org . t))) (setq org-confirm-babel-evaluate nil ;; Disable confirmation for code block evaluation org-export-with-smart-quotes t ;; turns " into curly quotes org-html-self-link-headlines t ;; headlines link to self org-html-validation-link nil ;; dont validate html (in order to check it is up to web standards) org-html-inline-images t ;; self-expl org-export-with-sub-superscripts t ;; render h_x as large h with subscript x ) (load-file (expand-file-name "macros.el" default-directory)) ;; Configure LaTeX image generation (setq org-latex-create-formula-image-program 'imagemagick) (defun my/get-org-roam-backlinks (filename) "Return backlinks for the given org-roam file. FILENAME should be the full path to the org-roam file." (save-excursion (with-current-buffer (find-file-noselect filename) (goto-char (point-min)) (if (and (featurep 'org-roam) (org-roam-node-at-point)) (let* ((node (org-roam-node-at-point)) (backlinks (org-roam-backlinks-get node))) (mapcar (lambda (backlink) (let ((source-node (org-roam-backlink-source-node backlink))) (list :file (org-roam-node-file source-node) :title (org-roam-node-title source-node) :content (with-current-buffer (find-file-noselect (org-roam-node-file source-node)) (goto-char (org-roam-backlink-point backlink)) (thing-at-point 'sentence t)) :link (format "[[id:%s][%s]]" (org-roam-node-id source-node) (org-roam-node-title source-node))))) backlinks) ) nil)))) (defun my/org-export-insert-backlinks (_backend) "Insert Org-roam backlinks before export, with HTML attributes." (when (and (featurep 'org-roam) (org-roam-node-at-point)) (let ((backlinks (my/get-org-roam-backlinks (buffer-file-name)))) (when backlinks (goto-char (point-max)) (insert "\n#+ATTR_HTML: :class backlinks-section :id backlinks\n Backlinks\n") (insert "#+ATTR_HTML: :class backlinks-list\n") (dolist (bl backlinks) (insert (format "- %s\n" (plist-get bl :link)))))))) (add-hook 'org-export-before-processing-hook #'my/org-export-insert-backlinks) (setq org-id-locations-file "~/.emacs.d/.org-id-locations") (setq org-roam-directory "~/master-folder/org_files/org_roam/") (org-id-update-id-locations) (setq org-html-link-home "") (setq org-html-link-up "") (defun z/generate-search-index (&rest _) "Generate search-index.json with title, url, and id." (when z/build-full-rebuild (message "Generating search-index.json...") (let* ((org-dir "~/master-folder/org_files/org_roam/") (out-file "~/master-folder/org_files/org_roam/output/search-index.json") (files (seq-filter (lambda (f) (not (string-match-p "/\\.#[^/]+\\.org$" f))) (directory-files-recursively org-dir "\\.org$"))) (index '())) (dolist (file files) (with-current-buffer (find-file-noselect file) (let ((id (org-id-get nil))) (when id (push `((title . ,(or (org-get-title) "Untitled")) (url . ,(concat "/" (file-name-base file) ".html")) (id . ,id)) index))))) ;; De-duplicate by ID (setq index (cl-remove-duplicates index :key (lambda (e) (cdr (assoc 'id e))) :test #'string=)) (with-temp-file out-file (insert (json-encode index))) (message "search-index.json generated (%d entries)." (length index))))) (add-hook 'org-publish-after-publishing-hook #'z/generate-search-index) (setq org-publish-project-alist `( ("org-roam" :base-directory "~/master-folder/org_files/org_roam/" :base-extension "org" :publishing-directory "~/master-folder/org_files/org_roam/output/" :recursive t :publishing-function org-html-publish-to-html :with-author nil :with-creator nil :with-date t :with-toc nil :section-numbers nil :html-preamble ,z-preamble :html-postamble ,z-postamble :html-head ,z-shared-head) ("org-static" :base-directory "~/master-folder/org_files/org_roam/assets/" :base-extension "css\\|js\\|png\\|jpg\\|gif" :publishing-directory "~/master-folder/org_files/org_roam/output/assets/" :recursive t :publishing-function org-publish-attachment))) ;(delete-directory "~/master-folder/org_files/org_roam/output/" t) (message "Directory deleted") (defun z/build-full () "Full rebuild: Org, assets, and search index." (interactive) (setq z/build-full-rebuild t) (org-id-update-id-locations) (z/generate-all-files-page) (org-publish "org-roam" t) (org-publish "org-static" t) (message "✅ Full rebuild complete")) (defun z/build-fast () "Fast rebuild: Org + assets only (no JSON)." (interactive) (setq z/build-full-rebuild nil) (z/generate-all-files-page) (org-publish "org-roam" t) (org-publish "org-static" t) (message "⚡ Fast rebuild complete (no search index)")) (org-id-update-id-locations) (cond ((member "--fast" command-line-args-left) (z/build-fast)) (t (z/build-full)))