;; build.el — org-publish configuration for personal site (require 'org) (require 'ox-publish) (require 'ox-html) (setq org-publish-use-timestamps-flag nil org-export-with-broken-links t org-html-validation-link nil org-html-head-include-default-style nil org-html-head-include-scripts nil) (setq site-nav "
aner@zakobar.com linkedin.com/in/aner-zakobar RSS
") (setq blog-nav "
← aner.zakobar.com
") (defun things-extract-keyword (file keyword) "Extract #+KEYWORD: value from FILE, or nil if absent/empty." (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (when (re-search-forward (concat "^#\\+" keyword ":[ \t]*\\(.*\\)$") nil t) (let ((v (string-trim (match-string 1)))) (unless (string= v "") v))))) (defun things-generate-page (_project) "Scan things/ and write content/generated/things-body.org." (let* ((things-dir "./things") (gen-dir "./content/generated") (files (when (file-directory-p things-dir) (file-expand-wildcards (concat things-dir "/*.org") t)))) (make-directory gen-dir t) (with-temp-file (concat gen-dir "/things-body.org") (insert "#+BEGIN_EXPORT html\n
\n") (dolist (file files) (let* ((title (things-extract-keyword file "THINGS_TITLE")) (desc (things-extract-keyword file "THINGS_DESC")) (path (things-extract-keyword file "THINGS_PATH")) ) (when (and title desc path) (insert (concat "
\n" "
\n" "

" title "

\n" "

" desc "

\n" "
\n" "
\n"))))) (insert "
\n#+END_EXPORT\n")))) (defun blog-escape-xml (str) "Escape XML special characters in STR." (let ((s (or str ""))) (setq s (replace-regexp-in-string "&" "&" s)) (setq s (replace-regexp-in-string "<" "<" s)) (setq s (replace-regexp-in-string ">" ">" s)) s)) (defun blog-date-to-rfc822 (date-str) "Convert YYYY-MM-DD string to RFC 822 format." (let* ((parts (split-string date-str "-")) (year (string-to-number (nth 0 parts))) (month (string-to-number (nth 1 parts))) (day (string-to-number (nth 2 parts))) (month-names '("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")) (day-names '("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat")) (time (encode-time 0 0 0 day month year)) (decoded (decode-time time)) (dow (nth 6 decoded))) (format "%s, %02d %s %04d 00:00:00 +0000" (nth dow day-names) day (nth (1- month) month-names) year))) (defun blog-generate-rss (_project) "Write public/feed.xml from all blog posts." (let* ((blog-dir "./content/blog") (base-url "https://aner.zakobar.com") (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-directory "./public" t) (with-temp-file "./public/feed.xml" (insert "\n") (insert "\n") (insert " \n") (insert " Aner Zakobar\n") (insert (format " %s\n" base-url)) (insert " Personal blog of Aner Zakobar\n") (insert (format " \n" base-url)) (dolist (file files) (let* ((base (file-name-base file)) (date (substring base 0 10)) (title (blog-escape-xml (blog-extract-title file))) (excerpt (blog-escape-xml (blog-extract-excerpt file))) (url (format "%s/blog/%s.html" base-url base))) (insert " \n") (insert (format " %s\n" title)) (insert (format " %s\n" url)) (insert (format " %s\n" url)) (insert (format " %s\n" (blog-date-to-rfc822 date))) (insert (format " %s\n" excerpt)) (insert " \n"))) (insert " \n") (insert "\n")))) (defun site-prepare (project) "Run all site preparation steps." (blog-generate-listings project) (blog-generate-rss project) (things-generate-page project)) (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"))))) (make-directory gen-dir t) (with-temp-file (concat gen-dir "/recent-posts.org") (let ((first t)) (dolist (file (seq-take files 3)) (unless first (insert "-----\n\n")) (insert (funcall make-entry file)) (setq first nil)))) (with-temp-file (concat gen-dir "/all-posts.org") (let ((first t)) (dolist (file files) (unless first (insert "-----\n\n")) (insert (funcall make-entry file)) (setq first nil)))))) (setq org-publish-project-alist `(("site-pages" :base-directory "./content" :base-extension "org" :publishing-directory "./public" :recursive t :exclude "generated/\\|blog/" :publishing-function org-html-publish-to-html :preparation-function site-prepare :html-head-include-default-style nil :html-head-include-scripts nil :html-head "" :html-preamble ,site-nav :html-postamble nil :with-author nil :with-creator nil :with-timestamps nil :section-numbers nil :with-toc nil) ("site-blog" :base-directory "./content/blog" :base-extension "org" :publishing-directory "./public/blog" :recursive nil :publishing-function org-html-publish-to-html :html-head-include-default-style nil :html-head-include-scripts nil :html-head "" :html-preamble ,blog-nav :html-postamble nil :with-author nil :with-creator nil :with-timestamps nil :section-numbers nil :with-toc nil) ("site-static" :base-directory "./static" :base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|svg\\|ico\\|woff2\\|woff\\|ttf" :publishing-directory "./public" :recursive t :publishing-function org-publish-attachment) ("site" :components ("site-pages" "site-blog" "site-static")))) (org-publish "site" t)