;;; build-site.el --- Publish my website using org-publish -*- lexical-binding: t; -*- ;; Author: Zaine Qayyum ;; Created: 2025-08-08 ;; Purpose: Build and publish my static site from Org files. ;;; Commentary: ;; Run this file with: ;; emacs -Q --script build-site.el ;;; Code: (require 'package) (defvar site-root (file-name-directory (or load-file-name buffer-file-name))) (defun site-path (path) (expand-file-name path site-root)) (add-to-list 'load-path (expand-file-name "lisp" (file-name-directory (or load-file-name buffer-file-name)))) (require 'recently-updated) (require 'tags) (require 'sidenotes) (require 'sitemaps) (setq package-user-dir (expand-file-name "./.packages")) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("elpa" . "https://elpa.gnu.org/packages/"))) (require 'ox-publish) (require 'cl-lib) (require 'org) (require 'ox) (require 'ox-html) (require 'ox-md) (package-initialize) (unless package-archive-contents (package-refresh-contents)) (package-install 'htmlize) (add-to-list 'load-path "~/master-folder/org_files/org_web/") (require 'htmlize) (defvar z-shared-head " " ) (setq org-export-global-macros (append '(("sidenote" . "@@html:$2@@") ("epigraph" . "@@html:
$1
$2
@@") ("epigraph_single" . "@@html:
$1
@@") ("epigraph3" . "@@html:
$1
$2, $3
@@") ("kbd" . "@@html:$1@@@@latex:\\texttt{$1}@@") ("margimg" . "@@html:@@") ("countdown" . "@@html:@@") org-export-global-macros))) (defvar z-preamble "
\"Site
Updated: %C
" ) (defvar z-postamble "") (defun z/comments-file-p (file) "Return non-nil if FILE has comments enabled. A file has comments if: - It has a #+COMMENTS: keyword with value \"t\" (case-insensitive)." (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (org-mode) (let* ((keywords (org-collect-keywords '("COMMENTS"))) (comments (cadr (assoc "COMMENTS" keywords)))) (print keywords) (and comments (string-match-p "^\\s-*t\\s-*$" (downcase comments))))))) (defun z/comments-file-slug (file) "Return page slug from #+SLUG: or fallback to filename." (with-temp-buffer (insert-file-contents file) (org-mode) (let* ((keywords (org-collect-keywords '("SLUG"))) (slug (cadr (assoc "SLUG" keywords)))) (if (and slug (string-match-p "\\S-" slug)) slug (file-name-base file))))) (defun z/org-html-insert-comments-into-body (body backend info) "Insert comments section at the end of the document body." (when (org-export-derived-backend-p backend 'html) (let ((input-file (plist-get info :input-file))) (if (and input-file (z/comments-file-p input-file)) (let* ((slug (z/comments-file-slug input-file)) (comments-html (format "

Comments

" slug))) (concat body comments-html)) body)))) (defun z/org-html-add-body-classes (output backend info) "Add layout-related classes to based on file metadata." (when (org-export-derived-backend-p backend 'html) (let* ((input-file (plist-get info :input-file)) (classes (delq nil (list (when (and input-file (z/wip-file-p input-file)) "wip") (when (and input-file (z/no-sidenotes-file-p input-file)) "no-sidenotes"))))) (if classes (replace-regexp-in-string "]*\\)>" (format "" (string-join classes " ")) output) output)))) (add-to-list 'org-export-filter-final-output-functions #'z/org-html-add-body-classes) (add-to-list 'org-export-filter-body-functions #'z/org-html-insert-comments-into-body) (defun z/lima--ensure-sitemap-file (project) "Ensure the sitemap source file exists under the project's base directory." (let* ((base-dir (file-name-as-directory (org-publish-property :base-directory project))) (fname (or (org-publish-property :sitemap-filename project) "lima-list.org")) (title (or (org-publish-property :sitemap-title project) "Lima")) (sitemap-path (expand-file-name fname base-dir))) ;; If file missing or you want to always regenerate, write header. (unless (file-exists-p sitemap-path) (with-temp-file sitemap-path (insert "#+TITLE: " title "\n" "#+OPTIONS: toc:nil num:nil\n\n"))))) (defun z/lima-sitemap-format-entry (entry style project) "Format sitemap entry for Lima; skip dirs, keep nested paths, and prefix /lima/." (let* ((file (if (listp entry) (car entry) entry)) (base-dir (file-name-as-directory (org-publish-property :base-directory project))) ;; Ensure we work with an absolute file path under base-dir (abs (if (file-name-absolute-p file) file (expand-file-name file base-dir)))) ;; Skip directories (unless (file-directory-p abs) (let* ((rel (file-relative-name abs base-dir)) ;; guaranteed no β€œβ€¦/..” backtracking now (rel-noext (file-name-sans-extension rel)) (title (org-publish-find-title abs project))) (format "[[file:%s.html][%s]]" rel-noext (or title (file-name-nondirectory rel-noext))))))) (defun z/copy-neighbor-attachments (source-dir pub-dir) "Copy sibling directories named .attachments.* from SOURCE-DIR to PUB-DIR." (let ((dirs (directory-files source-dir t "^\\.attachments\\..+"))) (dolist (d dirs) (when (file-directory-p d) (let* ((target (expand-file-name (file-name-nondirectory d) pub-dir))) (make-directory target t) ;; copy-directory: (DIRECTORY NEWNAME &optional KEEP-TIME PARENTS COPY-CONTENTS) (copy-directory d target t t t)))))) (defun z/publish-lima-file (plist filename pub-dir) "Publish Org or Markdown file from lima directory." (let* ((ext (downcase (or (file-name-extension filename) ""))) (base (file-name-base filename)) (src-dir (file-name-directory filename))) (cond ;; ORG FILES (publish as-is) ((string= ext "org") ;; Ensure sibling .attachments.* are published (z/copy-neighbor-attachments src-dir pub-dir) (org-publish-org-to 'z-html filename ".html" plist pub-dir)) ;; MARKDOWN FILES ((string= ext "md") (let* ((temp-org (expand-file-name (concat base ".org") (make-temp-file "lima-build-" t)))) ;; Convert Markdown β†’ Org with pandoc (call-process "pandoc" nil nil nil filename "-f" "markdown" "-t" "org" "-o" temp-org) ;; Ensure #+TITLE exists ;; Ensure front matter at top of temp-org (with-temp-buffer (insert-file-contents temp-org) ;; Build values (let* ((title base) (slug base) (date (format-time-string "<%Y-%m-%d %a %H:%M>"))) (goto-char (point-min)) ;; Insert front matter ;; (Always ensure they are at the absolute top) (insert "#+TITLE: " title "\n" "#+OPTIONS: num:nil\n" "#+DATE: " date "\n" "#+COMMENTS: t\n" "#+SLUG: " slug "\n\n")) ;; Write back to file (write-region (point-min) (point-max) temp-org)) ;; Ensure sibling .attachments.* are published (z/copy-neighbor-attachments src-dir pub-dir) ;; Publish using ORIGINAL base name (let ((output-file (expand-file-name (concat base ".html") pub-dir))) (org-publish-org-to 'z-html temp-org ".html" plist pub-dir) ;; temp-org base may differ; normalize to requested base (let ((generated (expand-file-name (concat (file-name-base temp-org) ".html") pub-dir))) (when (and (file-exists-p generated) (not (string-equal generated output-file))) (rename-file generated output-file t)))))) (t ;; Default: treat as attachment (org-publish-attachment plist filename pub-dir))))) (setq org-html-htmlize-output-type 'css) (org-export-define-derived-backend 'z-html 'html :filters-alist '((:filter-final-output . z/insert-filetags-after-title))) (defun z/org-main-prep (_project) (z/generate-recently-updated-org 26)) (defun z/z-publish-to-html (plist filename pub-dir) "Publish FILENAME to HTML using the z-html backend." (org-publish-org-to 'z-html filename ".html" plist pub-dir)) ;; Define the publishing project (setq org-publish-project-alist `(("org-main" :recursive t :base-directory ,(site-path "") :publishing-function z/z-publish-to-html :publishing-directory ,(site-path "output/") :base-extension "org" :auto-sitemap t :sitemap-filename "sitemap.org" :sitemap-title "Sitemap" :sitemap-sort-files chronologically :html-preamble ,z-preamble :html-postamble ,z-postamble :html-head ,z-shared-head :preparation-function z/org-main-prep) ("org-assets" :base-directory ,(site-path "assets/") :base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg\\|pdf\\|woff\\|woff2\\|ttf" :publishing-directory ,(site-path "output/assets/") :recursive t :publishing-function org-publish-attachment) ("org-categories-sitemap" :recursive t :base-directory ,(site-path "home/") :publishing-directory ,(site-path "output/home/") :base-extension "org" :auto-sitemap t :sitemap-filename "categories.org" :sitemap-title "Categories" :sitemap-function z/categories-sitemap :html-preamble ,z-preamble :html-postamble ,z-postamble :html-head ,z-shared-head) ("org-posts" :base-directory ,(site-path "posts/") :publishing-directory ,(site-path "output/posts/") :recursive t :base-extension "org" :publishing-function z/z-publish-to-html :with-author nil :with-creator nil :html-validation-link nil :with-toc t :section-numbers t :html-preamble ,z-preamble :html-postamble ,z-postamble :auto-sitemap t :sitemap-filename "posts-list.org" :sitemap-title "Posts List" :sitemap-style list :sitemap-function z/posts-sitemap :sitemap-sort-files anti-chronologically :html-head ,z-shared-head) ("org-blogs" :base-directory ,(site-path "blogs/") :publishing-directory ,(site-path "output/blogs/") :recursive t :base-extension "org" :publishing-function z/z-publish-to-html :with-author nil :with-creator nil :html-validation-link nil :html-preamble ,z-preamble :html-postamble ,z-postamble :auto-sitemap t :sitemap-filename "blogs-list.org" :sitemap-title "Blogs List" :sitemap-style list :sitemap-function z/blogs-grouped-sitemap :sitemap-sort-files anti-chronologically :html-head ,z-shared-head) ("org-books" :base-directory ,(site-path "books/") :publishing-directory ,(site-path "output/books/") :recursive t :base-extension "org" :publishing-function z/z-publish-to-html :html-preamble ,z-preamble :html-postamble ,z-postamble :auto-sitemap t :sitemap-filename "books-list.org" :sitemap-title "Books List" :sitemap-style list :sitemap-function z/books-sitemap :sitemap-sort-files anti-chronologically :html-head ,z-shared-head) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ("org-2025" ;; ;; :base-directory ,(site-path "blogs/2025/") ;; ;; :publishing-directory ,(site-path "output/blogs/2025/") ;; ;; :recursive t ;; ;; :base-extension "org" ;; ;; :publishing-function z/z-publish-to-html ;; ;; :html-preamble ,z-preamble ;; ;; :html-postamble ,z-postamble ;; ;; :auto-sitemap t ;; ;; :sitemap-filename "2025-list.org" ;; ;; :sitemap-title "2025 List" ;; ;; :sitemap-style list ;; ;; :sitemap-function z/2025-sitemap ;; ;; :sitemap-sort-files anti-chronologically ;; ;; :html-head ,z-shared-head) ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ("org-2026" ;; :base-directory ,(site-path "blogs/2026/") ;; :publishing-directory ,(site-path "output/blogs/2026/") ;; :recursive t ;; :base-extension "org" ;; :publishing-function z/z-publish-to-html ;; :html-preamble ,z-preamble ;; :html-postamble ,z-postamble ;; :auto-sitemap t ;; :sitemap-filename "2026-list.org" ;; :sitemap-title "2026 List" ;; :sitemap-style list ;; :sitemap-function z/2026-sitemap ;; :sitemap-sort-files anti-chronologically ;; :html-head ,z-shared-head) ("org-career" :base-directory ,(site-path "posts/career/") :publishing-directory ,(site-path "output/posts/career/") :recursive t :base-extension "org" :publishing-function z/z-publish-to-html :html-preamble ,z-preamble :html-postamble ,z-postamble :auto-sitemap t :sitemap-filename "career-list.org" :sitemap-title "Career List" :sitemap-style list :sitemap-function z/career-sitemap :sitemap-sort-files anti-chronologically :html-head ,z-shared-head) ("wip-pages" :base-directory ,(site-path "") :publishing-directory ,(site-path "output/") :recursive t :base-extension "org" :publishing-function z/z-publish-to-html :html-preamble ,z-preamble :html-postamble ,z-postamble :auto-sitemap t :sitemap-filename "wip.org" :sitemap-title "Work in progress" :sitemap-style list :sitemap-function z/wip-sitemap :sitemap-sort-files anti-chronologically :html-head ,z-shared-head) ("org-tags" :base-directory ,(site-path "tags/") :publishing-directory ,(site-path "output/tags/") :recursive t :base-extension "org" :publishing-function org-html-publish-to-html :html-preamble ,z-preamble :html-postamble ,z-postamble :html-head ,z-shared-head) ("org-lima" :base-directory ,(site-path "lima/") :publishing-directory ,(site-path "output/lima/") :recursive t :base-extension "org\\|md" :publishing-function z/publish-lima-file :with-author nil :with-creator nil :html-validation-link nil :html-preamble ,z-preamble :html-postamble ,z-postamble :sitemap-filename "lima-list.org" :auto-sitemap t :sitemap-title "Lima" :sitemap-style list :sitemap-sort-files anti-chronologically :sitemap-format-entry z/lima-sitemap-format-entry :html-head ,z-shared-head) )) (delete-directory (site-path "output/") t) (delete-directory (site-path "tags/") t) (message "Directory deleted") (z/write-tag-pages) (org-publish-all t) (message "Build complete!") ;;; build-site.el ends here