Converting To Org Publish

Motivation

I originally set up my personal website in early 2020 using blogdown since most of my work at the time was done with R or python, both well supported by Rmarkdown and its ecosystem. This was used in conjunction with the static site generator hugo to quickly make a nice looking site. It felt like there was a lot of configuration magic and hidden details, but after setting everything up once things just seemed to work.

For various reasons I didn’t write or change much about the site for a couple years. When I finally wanted to update the site I ran in to various errors from using an old Hugo version and there was a lot of configuration I just forgot about. I had been interested in using only emacs/org mode and thought this was a good time to finally go for it.

Setup for Makefile

;; below taken from systemcrafters
;; https://systemcrafters.net/publishing-websites-with-org-mode/building-the-site/
;; Set the package installation directory so that packages aren't stored in the
;; ~/.emacs.d/elpa path.
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

;; Initialize the package system
(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

;; Install use-package
(unless (package-installed-p 'use-package)
  (package-install 'use-package))
(require 'use-package)

;; Install other dependencies
(use-package htmlize
  :ensure t)

;; Load the publishing system
(require 'ox-publish)
(require 'shr)

(defun ejneer/publish ()
  "Helper to ignore publish cache"
  (interactive)
  (org-publish-all t))

Org Publish Configuration

(setq content-dir (expand-file-name "content/"))

(setq org-publish-project-alist
      (list
       (list "ejneer:main"
             :recursive t
             :base-directory content-dir
             :publishing-function 'ejneer/org-html-publish-to-html
             :publishing-directory (expand-file-name "public/")
             :with-author nil
             :with-creator nil
             :with-toc nil
             :section-numbers nil
             :time-stamp-file nil)

       (list "ejneer:static"
             :recursive t
             :base-directory (expand-file-name "content/")
             :publishing-directory (expand-file-name "public/")
             :publishing-function 'org-publish-attachment
             :base-extension "\\(gif\\|svg\\|css\\|jpeg\\|png\\|pdf\\)")

       (list "ejneer:all"
             :components '("ejneer:main" "ejneer:static"))))

(org-export-define-derived-backend 'ejneer-html
    'html
  :translate-alist '((template . ejneer/org-html-template)))

(defun ejneer/org-html-publish-to-html (plist filename pub-dir)
  (org-publish-org-to 'ejneer-html
                      filename
                      (concat "." (or (plist-get plist :html-extension)
                                      "html"))
                      plist
                      pub-dir))

HTML Template

I stumbled across the shr.el library that ships with emacs and use it to do all of the html generation for the site. I liked that it is essentially just writing raw html because function names correspond to html tags, but with the added ability to encapsulate elements in regular elisp functions and lists. This helped me in that I could approach the site in a piecewise manner and compose bits of it together. This also avoids having to learn any js frameworks which I have no interest in doing.

(defun ejneer/org-html-template (contents info)
  (concat
   "<!DOCTYPE html>"
   (shr-dom-to-xml
    `(html ((lang . "en"))
      ,(ejneer/site-head)
      ,(ejneer/site-body contents info)))))

(defun ejneer/site-head ()
  (shr-dom-to-xml
   `(head ()
     (meta ((charset . "utf-8")
            (author . "Eric Neer")
            (name . "viewport")
            (content . "width=device-width, initial-scale=1")))
     (title () ,(concat (org-export-data (plist-get info :title) info)) " - Eric Neer")
     (link ((href . "/static/styles.css")
            (rel . "stylesheet")))
     (link ((href . "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css")
            (rel . "stylesheet")))
     (link ((href . "/static/favicon.png")
            (rel . "icon")
            (type . "image/x-icon")))
     (script ((src . "https://polyfill.io/v3/polyfill.min.js?features=es6")))
     (script ((id . "MathJax-script")
              (async . "")
              (src . "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"))))))

(defun ejneer/site-body (contents info)
  (shr-dom-to-xml
   `(body ()
     (div ()
          ,(ejneer/site-sidebar info)
          (div ((class . "content"))
               ,contents)))))

(defun ejneer/site-sidebar (info)
  (shr-dom-to-xml
   `(div ((class . "sidebar"))
     (div ((class . "header"))
          (a ((href . "/"))
             (img ((src . "/static/pic.jpeg")
                   (width . "150")
                   (style . "border-radius: 50%;"))))
          (p ((class . "lead")) "Data Scientist & Engineer")
          (p ((class . "lead")) "Mercury Insurance")
          (br)
          ,(ejneer/site-sidebar-nav-link "fa-solid fa-id-card" "/cv.html" "CV")
          ,(ejneer/site-sidebar-nav-link "fa-brands fa-github" "https://github.com/ejneer" "GitHub")
          ,(ejneer/site-sidebar-nav-link "fa-brands fa-linkedin" "https://www.linkedin.com/in/eric-neer/" "LinkedIn")
          ,(ejneer/site-footer info)))))

(defun ejneer/site-sidebar-nav-link (icon link text)
  ;; simple helper to create a hyperlink with font awesome icon
  (shr-dom-to-xml
   `(div ((class . "sidebar-nav-item"))
     (i ((class . ,icon)))
     (a ((href . ,link)) ,text))))

(defun ejneer/site-footer (info)
  (shr-dom-to-xml
   `(div ((class . "footer"))
     (p () "Made with " ,(plist-get info :creator)))))

Helpers

I wanted to adopt the notion of a document type so I could treat the various org files differently as far as rendering them. The motivating need was that I wanted a list of blog posts on the main page, but not all org files are blog posts (or they are in-work blog posts) so they can’t just all be listed. This is implemented with a custom property doctype.

(defun ejneer/find-main-proj (proj-name)
  (cl-find-if (lambda (x) (string= (car x) proj-name)) org-publish-project-alist))

(setq ejneer/proj-name "ejneer:main")
(setq ejneer/proj-files (org-publish-get-base-files (ejneer/find-main-proj ejneer/proj-name)))

;; adapted from https://kitchingroup.cheme.cmu.edu/blog/2013/05/05/Getting-keyword-options-in-org-files/
(defun ejneer/get-file-keywords (file-path)
  (with-temp-buffer
    (insert-file-contents file-path)
    (org-element-map
        (org-element-parse-buffer 'element)
        'keyword
      (lambda (keyword) (cons (org-element-property :key keyword)
                              (org-element-property :value keyword))))))

(defun ejneer/is-post-p (file-path)
  (member '("PROPERTY" . "doctype post") (ejneer/get-file-keywords file-path)))

(defun ejneer/get-file-export-env (file-path)
  (with-temp-buffer
    (insert-file-contents file-path)
    (org-export-get-environment)))