;; 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
"
")
(setq blog-nav
"")
(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"
"
\n"
"
" desc "
\n"
"
\n"
"
\n")))))
(insert "
\n#+END_EXPORT\n"))))
(defun site-prepare (project)
"Run all site preparation steps."
(blog-generate-listings 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)