Publishing a website with Emacs and Org
Bad ideas can eat a lot of time
Why?
To be honest, I’m not quite sure why I thought this would be a good idea. Something like Hugo seems to be the obvious choice. But I already use Emacs for everything else so why not. Hugo also does a lot more than I need (which, let’s be honest, is basically nothing) so I’d have to read a lot of documentation on how to not use things.
Emacs provides a mechanism to generate a HTML page from org files with ox-publish. It’s not overly user friendly or all that well documented but you can mostly get it to work, if the use case is rather simple.
How does this work?
Most of the magic lies in publish.el
, as outlined here:
;; load up straight ;; the directory was already created by doom-emacs, might not fit for other setups ;; (defvar bootstrap-version) ;; (let ((bootstrap-file ;; (expand-file-name ".local/straight/repos/straight.el/bootstrap.el" user-emacs-directory)) ;; (bootstrap-version 6)) ;; (load bootstrap-file nil 'nomessage)) ;; ;; TODO: doom-emacs should have installed this, no idea why this is necessary ;; (straight-use-package 'htmlize) (require 'org) (require 'ox-publish) (setq user-full-name "Björn Erlwein") (defun bde/get-content (x) "Returns the contents of a file as a string" (with-temp-buffer (insert-file-contents x) (buffer-string))) (defun bde/sitemap-format-entry (entry style project) "Custom sitemap entry formatting with date" (format "[[file:%s][%s \[%s\]]]" entry (org-publish-find-title entry project) (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)))) (setq html-head (bde/get-content "./html/head.html")) (setq html-preamble (bde/get-content "./html/preamble.html")) (setq html-postamble (bde/get-content "./html/postamble.html")) ;; Some global settings that should apply everywhere, unless explicitely set locally (setq ;; Don't want 1. or 2. in front of every headline org-export-with-section-numbers nil org-export-with-smart-quotes t org-export-with-toc nil ;; This wraps the different sections in the specified html tags, useful for styling org-html-divs '((preamble "header" "top") (content "main" "content") (postamble "footer" "postamble")) org-html-container-element "section" ;; Creates a small comment with a timestamp, why not org-html-metadata-timestamp-format "%d.%m.%Y" ;; Create html5 instead of xhtml 4 org-html-html5-fancy t ;; For some reason not setting this will result in a xhtml doctype no matter if html5-fancy is set org-html-doctype "html5" ;; The default includes stuff I don't want/need, just use a fully custom styling org-html-head-include-default-style nil ;; these 3 replace the default sections with custom html files org-html-head html-head org-html-preamble html-preamble org-html-postamble html-postamble) ;; NOTE: Not used for now, static files are copied by the Makefile (defvar site-attachments (regexp-opt '("css")) "File types that are published as static files.") (setq org-publish-project-alist (list (list "bde-root" :base-directory "." :recursive t :publishing-function '(org-html-publish-to-html) :publishing-directory "./public" :exclude (regexp-opt '("README" "posts"))) (list "bde-posts" :base-directory "./posts" :publishing-directory "./public/posts" :recursive t :with-toc t ;; The sitemap will serve as an automatic list of all posts ;; there is also :makeindex but I have no idea what this is supposed to do... :auto-sitemap t :sitemap-filename "index.org" :sitemap-format-entry (lambda (entry style project) (bde/sitemap-format-entry entry style project)) ;; this has to be empty or it will be merged with the title of the index page... :sitemap-title "" ;; Newest posts should be at the top :sitemap-sort-files 'anti-chronologically) ;; (list "bde-static" ;; :base-directory "." ;; :exclude "public/" ;; :base-extension site-attachments ;; :publishing-directory "./public" ;; :publishing-function 'org-publish-attachment ;; :recursive t) ;; The order is important here ;; bde-posts creates posts/index.org which bde-root references (list "bde" :components '("bde-posts" "bde-root")))) (provide 'publish)
The project is then “built” with a small Makefile:
SITE_FILES := $(shell fd --extension org --extension html) STATIC_FILES := $(shell fd . ./static) STATIC_OUT_FILES := $(subst static,public/static,$(STATIC_FILES)) IMAGE_VERSION = 1.0.0 SERVER := [email protected] IDENTITY := ~/.ssh/webserver-nixos all: public/index.html public/static/%: static/% mkdir -p $(dir $@) cp $< $@ public/style.css: style.scss mkdir -p $(dir $@) sassc $< $@ public/index.html: publish.el public/style.css $(STATIC_OUT_FILES) $(SITE_FILES) # load the personal config # read the local source file # generate the html emacs --batch \ --eval "(package-initialize)" \ --eval '(load-file "$<")' \ --eval "(org-publish-all)" ############################## # This is for the dev server # ############################## server: http-server ./$< -c=css,html -i ./public http-server: curl -L https://github.com/TheWaWaR/simple-http-server/releases/download/v$(DEVSERVER_VERSION)/x86_64-unknown-linux-musl-simple-http-server -o $@ chmod +x @ publish: rsync -rauLv ./public -i $(IDENTITY) $(SERVER):/var/www/bde ssh -i $(IDENTITY) $(SERVER) 'chown -R nginx:nginx /var/www/bde' clean: -rm -rf public -rm -rf tmp -rm -rf out -rm -rf posts/index.org -rm -rf ${HOME}/.org-timestamps/bde-root.cache -rm -rf ${HOME}/.org-timestamps/bde-posts.cache -rm -rf ${HOME}/.org-timestamps/bde-static.cache .PHONY: server clean all publish
Everything is then packed up in a Docker container with Apache.
FROM httpd:alpine3.17 COPY ./public/ /usr/local/apache2/htdocs
Things I learned from doing this
This is really (really) slow without native-comp
My initial publish.el
didn’t use the packages from straight and also didn’t use the native-comp feature of Emacs.
This almost made me abort everything as publishing these few pages already took a minute.
Using my straight.el setup provided by Doom Emacs sped the process up to sub 1 second, which is really nice.
The org cache can be quite annoying
Org tries to be helpful and caches processed files to avoid doing useless work, which makes sense considering the previous point.
This seems to be a simple timestamp based process, similar to how make
handles things. Which would be fine in theory but it doesn’t consider changes to something like the html-head or the preamble, which is really annoying while trying to assemble a somewhat working site out of all this.