It’s been a while since I updated this site. Most of the old content was tailored for job applications. I have less reasons now to use this as a portfolio. Rather just a space to host my thoughts. Figured I will rebuild the site from scratch. I primarily write all my posts and notes in Org. Earlier I had successively used ox-hugo (+ Hugo) and blorg convert those to HTML. Nice and fast tools, but didn’t neatly fit into my workflow.
This time around I wanted to cut out all the middlemen and just use Emacs and Org to build this site. I have almost succeeded , but for a minor Python bit. There are a bazillion blogs and tutorials on using Emacs and Org to build a static site. I found this and this particularly useful. Crawling /r/emacs will give you dozens more. I have decided on the following to keep things relatively stable, and not reinvent the wheel:
- Use only built-in Emacs packages (I am using version 29+)
- Use a simple css
Use only Python standard library for the non-Emacs Lisp parts (I am using version 3.11)(No more Python dependency!)
This Emacs + Org + Python publishing system might be slower than some of the more popular static site generators. But in the absence of any benchmarks all I can say is that I really do not see any difference. On the other hand this system is self-contained, stable, and modular. My hope is that now that I have it set up, I will not have to tinker with it much.
Directory structure
The directory structure of this website is as follows (produced with M-x speedbar
which does not show hidden files like .htaccess
, etc.):
0:<+> css 0:<+> images 0:<+> posts 0:<+> tags 0:[+] 404.html * 0:[+] 404.org * 0:[+] Makefile * 0:[+] index.html * 0:[+] index.org * 0:[+] publish.el *
Additional CSS files live in the css
directory:
extra.css
adds some styling not handled bysimplecss
.syntax.css
produced withhtmlize-buffer
fromhtmlize.el
handles code highlighting.
Images used outside of posts, such as my profile picture, live in the images
directory.
Posts
All posts and any associated files, such as images, live in their own directories under the posts
directory. This setup of the posts makes it very easy to remove any post, and I do not need to worry about links to images and such. At the time of updating this post, the posts
directory looks like as follows:
0:<-> posts 1: <+> braketx 1: <-> how-this-website-is-created 2: [+] index.html * 2: [+] index.org * 1: [+] index.html * 1: [+] index.org *
Tags
The tags
directory is similarly arranged as the posts
directory, one sub-directory for each tag. Each sub-directory contains an index.org
file which lists all the posts corresponding to that tag. Originally I was using Python to create this, but now I have figured out an Emacs Lisp solution. To be honest I feel that the Emacs Lisp solution is simpler.
My original idea was:
- Build a hash table with tags as keys and list of corresponding posts as values.
- Create sub-directories for each tag under
tags
. - Write the contents of the hash table to the
index.org
files in the sub-directories undertags
. - Generate HTML files for the tags with Org publishing.
- Link each tag HTML file from its corresponding post HTML files.
The last bit required post-processing the generated HTML files. I failed to implement this with Emacs’s hash table. Implementing this with Python’s dict
and regex searches was simple. But it was a fragile system. Changes, whether unintentional or due to carelessness, in the generated HTML can lead to spurious regex matches. My current Emacs Lisp solution is:
- Collect the tags for a post.
- For each tag, if
tags/<tag>/index.org
exists, then append relative link to the post to it, else create that file and add relative link to the post to it. - Repeat this for all posts.
Linking tags from posts is handled by my header function which uses built-in Org variables. This bypasses both post-processing the generated HTML, and regex matches. When publishing, I just recursively delete the tags
directory and recreate it. This gets around the issue of checking existing links to posts for duplicates.
Publishing code
All the Emacs Lisp code are in publish.el
at the root of my pages directory. The main functions are:
my-html-preamble
- This creates the
<header>
element for each page. It is based on this. It automatically adds the date that each post was published on. If the post was updated later then it also adds that date. Additionally it adds links to the tags for that post. my-sitemap-format-entry
- This function defines how each entry in the list of posts look like. It adds links to the tags for each post.
my-create-tag-file-for-tag
- This function encodes the logic for step 2 of my new solution for tags.
my-create-tag-files-from-post-file
andmy-create-tag-files-from-directory
then loops this function over all tags and all posts.
;;; Publishing configuration -*- lexical-binding: t -*- (require 'package) (package-initialize) (require 'org) (require 'ox) (require 'ox-publish) (setq org-ditaa-jar-path "/usr/share/ditaa/ditaa.jar") (org-babel-do-load-languages 'org-babel-load-languages (append org-babel-load-languages '((ditaa . t)))) ;;; header / footer (defvar my-html-head-extra (concat "<link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>স</text></svg>\">\n" "<link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\">\n" "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/syntax.css\">\n" "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/extra.css\">\n")) (defun my-html-preamble (info) (let* ((date-format "%Y-%m-%d") (file (plist-get info :input-file)) (published (org-export-data (org-export-get-date info date-format) info)) (updated (format-time-string date-format (file-attribute-modification-time (file-attributes file)))) (title (org-get-title info))) (concat "<nav>\n" "<a href=\"/\" alt=\"Go home\">home</a>\n" "<a href=\"/posts/\" alt=\"Read all posts\">posts</a>\n" "</nav>\n" "<h1 class=\"title\">" title "</h1>\n" (unless (or (equal title "~") (equal title "~/posts") (string-match-p (regexp-quote "tags") (directory-file-name file))) (concat "<p class=\"date\">Published on " published (unless (equal updated published) (concat " (updated on " updated ")")) "</p>\n")) (when org-file-tags (concat "<p class=\"tags\">tags: " (mapconcat '(lambda (tag) (concat "<a class=\"tag\" href=\"../../tags/" tag "/index.html\">" tag "</a>")) (flatten-list org-file-tags) " ") "</p>\n"))))) (defvar my-html-postamble (format-time-string "<p>Copyright © 2020 — %Y Soham Pal</p>\n")) ;;; Sitemap (defun my-sitemap-format-entry (entry style project) (let ((title (org-publish-find-title entry project)) (tags (org-publish-find-property entry :filetags project))) (concat "[[file:" entry "][" title "]]" (when tags (concat " (" (mapconcat '(lambda (tag) (concat "[[file:../tags/" tag "/index.org][" tag "]]")) (flatten-list tags) " ") ")"))))) ;;; Tags (defun my-create-tag-file-for-tag (tag post-file) (let* ((tag-file (file-name-concat "tags" tag "index.org")) (relpath (file-relative-name post-file (file-name-directory tag-file))) (title (org-get-title post-file))) (if (file-exists-p tag-file) (write-region (concat "- [[file:" relpath "][" title "]]\n") nil tag-file :append) (progn (make-directory (file-name-directory tag-file) :parents) (with-temp-file tag-file (insert (concat "#+title:" tag "\n")) (insert "\n") (insert (concat "- [[file:" relpath "][" title "]]\n"))))))) (defun my-create-tag-files-from-post-file (post-file) (let ((tags (with-temp-buffer (insert-file-contents post-file) (org-mode) org-file-tags))) (when tags (dolist (tag (flatten-list tags)) (my-create-tag-file-for-tag tag post-file))))) (defun my-create-tag-files-from-directory (dir) (let ((files (directory-files-recursively dir ".org"))) (dolist (file files) (my-create-tag-files-from-post-file file)))) ;;; Common publish settings (setq org-export-allow-bind-keywords t org-export-babel-evaluate nil org-export-with-author nil org-export-with-creator nil org-export-with-footnotes t org-export-with-latex t org-export-with-smart-quotes t org-export-with-section-numbers nil org-export-with-toc nil org-export-with-title nil org-html-container-element "main" org-html-doctype "html5" org-html-divs '((preamble "header" "preamble") (content "main" "content") (postamble "footer" "postamble")) org-html-head-include-default-style nil org-html-head-extra my-html-head-extra org-html-html5-fancy t org-html-htmlize-output-type "css" org-html-link-home "" org-html-link-up "" org-html-preamble 'my-html-preamble org-html-postamble my-html-postamble org-html-validation-link nil) ;;; Export (setq org-publish-project-alist `(("org-posts" :base-directory "./posts/" :base-extension "org" :publishing-directory "./posts" :recursive t :publishing-function org-html-publish-to-html :auto-sitemap t :sitemap-filename "index.org" :sitemap-style list :sitemap-format-entry my-sitemap-format-entry :sitemap-sort-files anti-chronologically :sitemap-title "~/posts") ("org-tags" :base-directory "./tags/" :base-extension "org" :publishing-directory "./tags" :recursive t :publishing-function org-html-publish-to-html) ("org-home" :base-directory "./" :base-extension "org" :publishing-directory "./" :recursive nil :publishing-function org-html-publish-to-html))) ;;; Publish (when (file-exists-p "tags") (delete-directory "tags" :recursive nil)) (my-create-tag-files-from-directory "posts") (org-publish-all)
Makefile
Finally I have a Makefile to semi-automate the whole process. It has directives to:
- Run
publish.el
with Emacs in batch mode. - Delete the Org cache.
- Delete the Org cache, then run
publish.el
with Emacs in batch mode.
However usually I just find myself doing C-c C-e
in a buffer visiting publish.el
.
build: emacs -Q --batch --script publish.el clean: rm ~/.org-timestamps/org-* clean_and_build: | clean build