DrolleryMedieval drollery of a knight on a horse
flowery border with man falling
flowery border with man falling

Introduction

This is my org-publish build script. I was just writing directly to the build.el file, but then I started thinking about it, and that made no sense. If I’m using Orgmode to write a blog, I should be using it to write the thing that writes that thing that builds the blog! It’s elementary!

Why a build.el and not just include this in my emacs config so that it all automatically runs when I publish through emacs? Because my workflow doesn’t include me publishing via emacs. Instead I git add . && git commit -m "Updates!" && git push and then expect that my CI/CD will build and deploy my website for me. Because of this expectation, I’d rather have the org-publish related stuff separate from my emacs configuration so that the CI/CD has a much easier time setting up emacs. Plus, when I do want to build the site locally I just have to run sh build.sh which then will trigger build.el.

Resources

Alternatives

README.org

It seems a little weird to build an org file, with an org file. I’m really only doing this because the build directory is entry built from this org file, but the intention is that I compile the files in build locally and commit them. There is a chance that someone — including future me — might come upon this build directory and be uninformed about it’s creation and use.

#+title: Readme

NOTICE: This file and all files in this directory were built with =build.org=
and should not be directly edited!

This is the build dir for use with org-publish. It should work, but I'm not
using it at the moment. Should just have to run `./build.sh` and it will spit
out the org files in the parent directory into a `./_html/` dir.

To use the Dockerfile, you'll need to be in the parent directory, aka ~org/~,
and then you will run =docker build -t test -f build/Dockerfile .=

The final nginx container has the http port exposed.

build.sh

This is just a little shell script we’re using to make it easy to call the elisp code that does the real work of building the website.

rm -rf ./_html/
rm -rf ./_html/ ./org-timestamps/
mkdir ./org-timestamps
emacs -Q --script build.el
cp ./*.css ./_html/

build.el

Now onto the main attraction, build.el will do all the heavy lifting of building the website.

Front Matter

Just a little front matter to describe the software, probably pointless really, but it’s here just in-case. I’ll probably eventually fill in the commentary and description…

;;; build.el --- Description -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2022 Ian S. Pringle
;;
;; Author: Ian S. Pringle <[email protected]>
;; Maintainer: Ian S. Pringle <[email protected]>
;; Created: August 02, 2022
;; Modified: August 24, 2022
;; Version: 0.2.0
;; Keywords: bib convenience docs files hypermedia lisp outlines processes tools
;; Homepage: https://github.com/pard68/org
;; Package-Requires: ((emacs "24.4"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;;  Description
;;
;;; Code:

Make sh-set-shell quiet

The sh-set-shell gets called on files when there is shell scripts in it. I’m not sure what it does but it is very noisy and I dislike the noise. So we can inhibit it with this:

(advice-add 'sh-set-shell :around
            (lambda (orig-fun &rest args)
              (let ((inhibit-message t))
                (apply orig-fun args))))

Dependencies

We need to be able to install some dependencies, since we can’t count on the emacs.d directory having them installed already during CI/CD, plus we can also separate this from our packages we use for normal, everyday emacs, which means we can depend on different versions or even on things we don’t want polluting the rest of our setup.

straight.el

I’m messing around with using straight.el in addition to use-package because the package ox-attach-publish is not on Melpa or any other package repo currently. If this works well, I will work on refactoring the above dependencies to use straight.el instead of package.el.

This will bootstrap straight.el, I got it straight from their git repo:

(setq package-enable-at-startup nil)

(setq straight-build-dir (expand-file-name "./.packages"))
(setq straight-use-package-by-default t)

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package 'use-package)

Getting And Requiring Packages

Now that we can use-package we can get on with, well, using packages. Let’s start with org and htmlize. Org is… well it’s org! and htmlize is more or less pygments or highlight.js, but for org-publish.

;; Install needed packages
(use-package org)
(use-package htmlize)
(require 'ox-publish)
(require 'ox-html)
(require 'htmlize)

s is a package for handling strings, f is a package for working with files. I’m not actually using either right now, I just am leaving this here (untangled) as a reminder to myself to invest some time into making use of it at a future date.

(use-package s)
(use-package f)
(require 's)
(require 'f)

ox-attach-publish is a tool that converts attached files into valid links for orgmode to then export:

(use-package ox-attach-publish
  :straight '(ox-attach-publish
              :type git
              :host github
              :repo "simoninireland/ox-attach-publish"))
(require 'ox-attach-publish)

We require org-roam to build and parse the org-roam related files I have. Specifically, I’m looking to use this to generate the links and backlinks between those files.

(use-package org-roam)
(require 'org-roam)

This doesn’t work because it creates a new export type, but I’m leaving it here to maybe one day figure out…

(use-package org-tufte
  :straight '(org-tufte
              :type git
              :host github
              :repo "Zilong-Li/org-tufte")
  :ensure nil
  :config
        (require 'org-tufte)
        (setq org-tufte-htmlize-code t
              org-tufte-embed-images t))

Finally, we’ll require everything we need, including some things that we didn’t have to download first:

(require 'find-lisp)

The Meat and Potatoes

This is ephemeral, we don’t need no stinking backups!

(setq make-backup-files nil)

Common Variables

Now to setup some variables for later use:

(defvar build--build-dir (getenv "PWD"))
(defvar build--project-dir (concat build--build-dir "/.."))
(defvar build-publish-dir (concat build--build-dir "/_html"))
(defvar build--site-name "Drollery")
(defvar build--publish-url "https://drollery.org")
(defvar build--date-format "%Y-%m-%d")

Initialize org-roam

We initialize the org-roam project and DB so that we can lean on it later to generate backlinks.

(setq org-roam-directory build--project-dir
      org-roam-db-location (concat build--project-dir "/org-roam.db"))

(org-roam-update-org-id-locations)
;; (require 'org-roam-export)

org-publish settings

There are some settings we need to tweak to get org-publish and ox-publish working the way we want.

This sets the org-timestamps dir to something local to our build dir. It’s probably not needed, but I like to keep this together to reduce on clutter since the default is ~/

(setq org-publish-timestamp-directory (concat build--build-dir "/org-timestamps/")

Now we’ll set some default HTML stuff. First is the `org-html-divs` alist which tells org-export what html element and id to use for the preamble, content, and postamble on each page:

org-html-divs '((preamble "header" "preamble")
                      (content "main" "content")
                      (postamble "footer" "postamble"))
org-html-container-element "section"
org-html-metadata-timestamp-format build--date-format
org-html-checkbox-type 'ascii
org-html-html5-fancy t
org-html-doctype "html5"
org-html-htmlize-output-type 'css
org-html-fontify-natively t)

Here we turn off inlining CSS and then inject our own CSS into the <head>

(defvar build--html-head
  (concat
   "<link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\">"
   "<link rel=\"stylesheet\" type=\"text/css\" href=\"https://gongzhitaao.org/orgcss/org.css\" />"
   "<link rel=\"stylesheet\" href=\"/style.css\" type=\"text/css\" />"
   "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=IM+Fell+English\" />"))

Some macros

(setq org-export-global-macros
      '(("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@")
        ("h" . "@@html:$1@@")))

Sitemap Maker

We’ll use this later to setup our /{dir}/index.html pages, for example to list all blog posts:

(defun build--org-sitemap-date-entry-format (entry _ project)
  "Build sitemap/index for a number of pages. Format ENTRY in org-publish
  PROJECT Sitemap format ENTRY ENTRY STYLE format that includes date."
  (format "{{{h(<span class=\"sitemap-entry\">)}}}{{{timestamp(%s)}}} [[file:%s][%s]]{{{h(</span>)}}}"
          (format-time-string build--date-format (org-publish-find-date entry project))
          entry
          (org-publish-find-title entry project)))

CSS Inliner

This inlines CSS, but I’m not using it right now. Eventually I’d like to pursue optimizations that would allow for in-lining critical CSS and then throwing the rest into the styles.css.

(defun my-org-inline-css-hook (exporter)
  (when (eq exporter 'html)
    (let* ((dir (ignore-errors (file-name-directory (buffer-file-name))))
           (path (concat dir "style.css"))
           (homestyle (or (null dir) (null (file-exists-p path))))
           (final (if homestyle (concat build--build-dir "/style.css") path))) ;; <- set your own style file path
      (setq org-html-head-include-default-style nil)
      (setq org-html-head (concat
                           "<style type=\"text/css\">\n"
                           "<!--/*--><![CDATA[/*><!--*/\n"
                           (with-temp-buffer
                             (insert-file-contents final)
                             (buffer-string))
                           "/*]]>*/-->\n"
                           "</style>\n")))))

(add-hook 'org-export-before-processing-hook 'my-org-inline-css-hook)

We’ll use this later on as the preamble for every page:

(defvar build--nav-bar "<nav><a href=\"/index.html\">index</a>
                |
                <a href=\"/about.html\">about</a>
                |
                <a href=\"/blog/index.html\">blog</a>
                |
                <a href=\"/grok/grok.html\">grok</a>")
(defvar build--logo
  (concat "<pre id=\"logo\">"
          (shell-command-to-string (concat "figlet " build--site-name))
          "</pre>"))

(defvar build--header (concat build--logo build--nav-bar))

This is the footer or postamble:

(defvar build--footer-left "<div id=\"footer-left\">
<p class=\"author\">Author: Ian S. Pringle</p>
<p class=\"date\">Site Updated: %T</p>
<p class=\"creator\">Created with %c</p>
</div>")

(defvar build--footer-mid "<div id=\"footer-mid\">
<img class=\"fleuron\" src=\"/fleuron.svg\"><img/>
</div>")

(defvar build--footer-right "<div id=\"footer-right\">
<p class=\"copyright-notice\">Creative Commons</p>
<a href=\"http://creativecommons.org/licenses/by-nc-sa/4.0/\">BY-NC-SA</a>
</div>")

(defvar build--scripts "<script type='module'>
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>")

(defvar build--footer (concat build--footer-left build--footer-mid build--footer-right))

org-publish projects

We’ll now build out the projects. Each “project” is a like a group of pages. So there is a “blog” project for all the stuff under blog directory, for example. I think I could pull this off with just one project, however I’d only get one index page and I want an index page for the blog posts and another for the kb and a third for everything.

(setq org-html-validation-link nil)
(setq org-publish-project-alist
      (list

“pages” project

(list "pages"
      :base-directory build--project-dir
      :publishing-directory build-publish-dir
      :publishing-function 'org-attach-publish-to-html
      :html-head-extra build--html-head
      :html-preamble build--header
      :html-postamble build--footer
      :html-head-include-default-style nil
      :auto-sitemap t
      :sitemap-filename "pages.org"
      :sitemap-format-entry 'build--org-sitemap-date-entry-format
      :sitemap-sort-files 'alphabetically
      :recursive nil
      :with-author t
      :with-creator t
      :with-drawer t
      :with-toc nil
      :section-numbers nil
      :exclude-tags (list "private" "draft")
      :exclude "life.org\\|dates.org\\|journal.org\\|jha.org\\|weekly.org\\|bible-plan.org")

“grok” project

(list "grok"
      :base-directory (concat build--project-dir "/grok")
      :publishing-directory (concat build-publish-dir "/grok")
      :publishing-function 'org-attach-publish-to-html
      :html-head-extra build--html-head
      :html-preamble build--header
      :html-postamble build--footer
      :html-head-include-default-style nil
      :auto-sitemap t
      :sitemap-filename "index.org"
      :sitemap-format-entry 'build--org-sitemap-date-entry-format
      :sitemap-sort-files 'alphabetically
      :with-author t
      :with-creator t
      :with-drawer t
      :with-toc nil
      :section-numbers nil)

“blog” project

This contains all the files in the blog dir. I eventually would like to figure out how to make this work with a single blog.org file and then each heading or maybe subheading in that file is a “page” on the site.

(list "blog"
      :base-directory (concat build--project-dir "/blog")
      :publishing-directory (concat build-publish-dir "/blog")
      :publishing-function 'org-attach-publish-to-html
      :attachments-project "static"
      :attachments-base-directory "files"
      :html-head-extra build--html-head
      :html-preamble build--header
      :html-postamble build--footer
      :html-head-include-default-style nil
      :auto-sitemap t
      :sitemap-filename "index.org"
      :sitemap-format-entry 'build--org-sitemap-date-entry-format
      :sitemap-sort-files 'anti-chronologically
      :with-author t
      :with-creator t
      :with-drawer t
      :with-toc nil
      :section-numbers nil)

“books” project

This contains all the files in the blog dir. I eventually would like to figure out how to make this work with a single blog.org file and then each heading or maybe subheading in that file is a “page” on the site.

(list "books"
      :base-directory (concat build--project-dir "/books")
      :publishing-directory (concat build-publish-dir "/books")
      :publishing-function 'org-attach-publish-to-html
      :attachments-project "static"
      :attachments-base-directory "files"
      :html-head-extra build--html-head
      :html-preamble build--header
      :html-postamble build--footer
      :html-head-include-default-style nil
      :auto-sitemap t
      :sitemap-filename "index.org"
      :sitemap-format-entry 'build--org-sitemap-date-entry-format
      :sitemap-sort-files 'alphabetically
      :with-author t
      :with-creator t
      :with-drawer t
      :with-toc nil
      :section-numbers nil)

“static” project

This contains all the static content in my org directory.

(list "static"
      :base-directory "~/org/"
      :base-extension "txt\\|jpg\\|jpeg\\|png\\|svg\\|gif\\|js"
      :recursive t
      :publishing-directory build-publish-dir
      :publishing-function 'org-publish-attachment)

“assets” project

This contains all the assets in my org/build directory.

(list "assets"
      :base-directory "~/org/build"
      :base-extension "css\\|js\\|svg"
      :recursive nil
      :publishing-directory build-publish-dir
      :publishing-function 'org-publish-attachment)

“site” project

This is just a “meta” project that contains all the above projects as components:

(list "site"
      :components (list "pages" "grok" "blog" "books" "static" "assets")
      :auto-sitemap t
      :sitemap-filename "sitemap.org"
      :sitemap-format-entry 'build--org-sitemap-date-entry-format
      :sitemap-sort-files 'anti-chronologically
      :html-doctype "html5"
      :html-html5-fancy t)))

Extras

This is not working atm

(defun build--collect-backlinks-string (backend)
  "Insert backlinks into the end of the org file before parsing it."
  (when (org-roam-node-at-point)
    (goto-char (point-max))
    ;; Add a new header for the references
    (insert "\n** Xrefs\n")
    (let* ((backlinks (org-roam-backlinks-get (org-roam-node-at-point))))
      (dolist (backlink backlinks)
        (let* ((source-node (org-roam-backlink-source-node backlink))
               (point (org-roam-backlink-point backlink)))
          (insert
           (format "- [[./%s][%s]]\n"
                   (file-name-nondirectory (org-roam-node-file source-node))
                   (org-roam-node-title source-node))))))))

(defun build--add-extra-sections (backend)
  (when (org-roam-node-at-point)
    (save-excursion
      (goto-char (point-max))
      (build--collect-backlinks-string backend)
      (insert "\n** Mentions\n\n")
      (insert "#+BEGIN_EXPORT html
<div id='webmentions'></div>
#+END_EXPORT"))))

(add-hook 'org-export-before-processing-hook 'build--add-extra-sections)

org-publish

And finally, we build the project!

(org-publish "site" t)
(message "Build completed!")
(provide 'build)
;;; build.el ends here

style.css

html,
body {
  height: 100%;
  width: 100%;
}

html {
  font-family: "IM Fell English";
  text-rendering: geometricPrecision;
  -webkit-font-smoothing: antialiased;
}

body {
  display: flex;
  flex-direction: column;
  font-family: unset;
  font-size: 15px;
  margin: unset;
}

h1, h2, h3, h4, h5, h6 {
  color: unset;
  font-family: unset;
}

#content {
  flex: 1 0 auto;
  margin: auto;
  max-width: min(669px, 60vw);
  font-size: 1.5rem;
  line-height: 2rem;
}

#preamble {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: unset;
}

#preamble.status {
  margin: unset;
}

#preamble nav {
  font-size: 2em;
}

#preamble #logo {
  background-color: white;
  border: unset;
  font-size: 1.25em;
  padding: unset;
  width: fit-content;
  font-family: monospace;
}

#postamble {
  display: flex;
  flex-direction: row;
  align-items: center;
  width: 100%;
  justify-content: space-between;
  font-size: 0.7em;
  line-height: 1em;
}

#postamble * {
  margin: 5px;
}

#postamble p {
  margin: unset;
}

#postamble div:first-child,
#postamble div:last-child {
  display: flex;
  flex-direction: column;
  flex: 1;
}

#postamble div:last-child * {
  align-self: flex-end;
}

#postamble div:nth-child(2) {
  flex: 0 0 auto;
}

.sitemap-entry {
  display: flex;
  gap: 1rem;
}

.sitemap-entry > :last-child {
  flex: 1;
}

Here’s a few things to make the site nicer on smaller devices:

@media (max-width: 669px) {
    #content {
        margin: 0 1em;
        max-width: unset;
    }
}

fleuron.svg

Since SVGs are just XML, I can document my favicon/fleuron in the build doc here, it’ll generate and be exported when I tangle the doc. Pretty neat!

<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="20"
height="20" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality"
shape-rendering="geometricPrecision" text-rendering="geometricPrecision"> <path
    d="M9 0v1h-.5v2H8V2H7v-.5h-.5V1H6V.5H3.5V1H3v.5h-.5V2H2v.5h-.5V3H1v1H0v2.5h.5V7h1v-.5H2V6h1v-.5h-.5V5H2V4h-.5v-.5H2V3h.5v-.5H3V2h1v-.5h.5V1H5v1h1v.5h1.5v1H8V4h1v.5h1V5h1.5v1H11v1h-1v1h.5v1h.5v.5h-.5v.5H10V8.5h-.5V8H9v-.5H8V7h-.5v-.5H6V7h-.5v.5h-1v1h-1V10H3v3.5h.5v2h1v1H5v.5h.5v.5H6v.5h.5v.5H7v.5h1v.5h2.5v.5h3v-.5H15V19h.5v-.5h.5V18h.5v-.5h.5V17h-1v.5h-.5v-1H15v.5h-.5v.5H14v1h-.5v.5H13v.5h-2V19h-.5v-2h.5v-.5h1V16h1v-.5h1.5V15h.5v-.5h.5V14h.5v-.5h.5V12h.5v-.5h.5V9H17v-.5h-.5V8H16v-.5h-3.5V8H12v.5h-1v-1h.5v-1h.5v-1h1V6h.5v.5h2V6h.5v-.5h1V5h1V4h.5v-.5h.5V3h.5v-.5h.5V2h-.5v-.5H19V1h-.5V.5H18V0h-.5v.5H17V1h-1v.5h-.5V2h.5v.5h.5V3h1v.5H17V4h.5v.5h-1V5h-1v.5H14V5h-2V4h.5V2.5H12V1h-1V0H9.5zm6.5 16.5h.5V16h-.5ZM10 .5h.5v1h.5V2h.5v.5H11v1h.5v1h-1V4H10v-.5h-.5V3H9V2h.5V1h.5Z">
</path> </svg>

Dockerfile

This is the Dockerfile that will run the build.el build script, and then put that into an nginx container for hosting or testing. Should be noted that this needs to run from the parent directory to build, in this particular case that means in ~/org/..

Start by grabbing silex/emacs and name it ‘builder’.

FROM silex/emacs AS builder

Make the working directory /org. Then make the emacs.d directory, this is mostly useless but it ensures it exists when we install stuff with our use-package in build.el. Then we just add some extra dependencies. I am not sure if I even need build-essential… we need sqlite3 to build the org-roam database. And git-restorem-time is currently unused but I think I will eventually make use of it and so I’m just leaving it here as a reminder:

WORKDIR /org
RUN mkdir -p ~/.emacs.d/private/ && apt-get update && apt-get --yes install build-essential sqlite3 git-restore-mtime

Next, copy the entire org directory to the working directory, cd into build/ and kick off the build.sh script:

COPY .. .
run cd ./build/ && ./build.sh

Finamly, create an nginx container named ‘server’, copy the statically compiled assets to it, and then annotate port 80 as the port to use.

from nginx as server
copy --from=builder /org/build/_html/ /usr/share/nginx/html/
expose 80