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
https://commonplace.doubleloop.net/how-i-publish-my-wiki-with-org-publish
https://www.ereslibre.es/blog/2019/09/building-a-personal-website-with-org-mode.html
https://orgmode.org/manual/Advanced-Export-Configuration.html
https://meganrenae21.github.io/Meg-in-Progress/posts/blogging-with-org-mode.html
https://web.archive.org/web/20201107234305/https://vicarie.in/posts/blogging-with-org.html
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)
<head>
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)
Navbar / Header
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))
Footer
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
TODO Backlinks
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 /org/build/_html/ /usr/share/nginx/html/
expose 80