1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
#+title: On Switching to Org-Mode for my Website
#+author: Jacob Janzen
#+date: <2024-04-14 Sun>
When I first created this website, I built everything in raw HTML. I liked the control I had over it. It was also a real pain to update it though. Components such as the navigation bar at the top had to be copied to every HTML file along with CSS styling. This could have been made easier with JavaScript, but I consider that to be an abuse of scripting. One of the primary design decisions of this site was avoiding JavaScript whenever possible and there had to be another way.
Enter Emacs Org-Mode.
Emacs Org-Mode at its core is a very powerful note-taking software with a lot of depth that I have not come close to. What I do know is that it is quite a bit more ergonomic than Markdown for my needs which is why I prefer =.org= over =.md= whenever I have to write documentation. I've been using Emacs for a while, but hadn't really considered its utility as a highly configurable static site generator before now. Having done so though, I have to say that I really enjoy this new setup. I have set up a [[https://git.sr.ht/~jjanzen/website/tree/main/item/.build.yml][build script]] using sourcehut's build configuration that automatically compiles my =.org= files into =.html= and publishes it. The key line is
#+begin_src sh
emacs --batch -f package-initialize --script ~/website/publish.el
#+end_src
which calls Emacs with a [[https://git.sr.ht/~jjanzen/website/tree/main/item/publish.el][publishing script]] I made in Emacs Lisp.
This =elisp= script that is called is a fair bit more complicated than the =.build.yml= file and it definitely took me a while to get it to where I was happy with it. First, I ensure I have installed all the packages I need with =use-package=.
#+begin_src emacs-lisp
(require 'package)
(add-to-list 'package-archives '("gnu" . "https://elpa.gnu.org/packages/"))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(package-initialize)
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(eval-and-compile
(setq use-package-always-ensure t
use-package-expand-minimally t))
(use-package org
:ensure org-plus-contrib)
(use-package ox-rss
:after org)
(require 'org)
(require 'ox-html)
#+end_src
=use-package= is maybe a little heavier than what I need for package management in a script like this, but =ox-rss=, the package used to generate my RSS feed was a challenge to get installed correctly. It must be installed *after* =org= is set up.
The core of publishing =.org= files to =.html= is the =org-publish-project-alist= variable. It allows me to define different sources for the site, each with different configuration options.
The first source contains the core of my website. Most of the pages outside of my blog appear here. These all reside in the same repository as my website build scripts. Also note the =:html-preamble= and =:html-head= fields. The ability to include my navigation bar and style-sheet in every file automatically made things so much nicer for me.
#+begin_src emacs-lisp
("org-core"
:base-directory "~/website/"
:base-extension "org"
:publishing-directory "~/public_html/"
:recursive t
:publishing-function org-html-publish-to-html
:with-toc nil
:headline-levels 4
:section-numbers nil
:html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
:html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
:html-postamble nil)
#+end_src
Also in this repository, I have my styling and images (at this point it's literally a single =.css= file) which is included from the second entry.
#+begin_src emacs-lisp
("org-static"
:base-directory "~/website/"
:base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
:publishing-directory "~/public_html"
:recursive t
:publishing-function org-publish-attachment)
#+end_src
The blog generation was something of an abuse of Org-Mode's publishing features. It includes the ability to generate a sitemap automatically. I don't particularly need (or want) a true sitemap though. This entry is largely the same as my core website configuration, but I have it generate my sitemap into a file called =index.org= which is then compiled into HTML like every other page allowing me to have [[https://jjanzen.ca/blog]] point to a list of every page in my blog repository and have that list update automatically.
#+begin_src emacs-lisp
("org-blog"
:base-directory "~/blog/"
:base-extension "org"
:publishing-directory "~/public_html/blog"
:recursive t
:publishing-function org-html-publish-to-html
:with-toc nil
:with-properties t
:with-date t
:with-timestamps nil
:headline-levels 4
:section-numbers nil
:html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
:html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
:html-postamble nil
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-format-entry my/org-publish-org-sitemap-format
:sitemap-sort-files anti-chronologically
:sitemap-title "Blog")
#+end_src
The default formatting for that list just shows the title, but I do think it's valuable to also list the date for blog posts. To remedy this, I define my own sitemap format function here.
#+begin_src emacs-lisp
(defun my/org-publish-org-sitemap-format (entry style project)
(cond ((not (directory-name-p entry))
(format "(%s) [[file:blog/%s][%s]]\n"
(format-time-string "%Y-%m-%d"
(org-publish-find-date entry project))
entry
(org-publish-find-title entry project)))))
#+end_src
The most challenging portion to implement was the RSS feed though. This was a feature I wanted and never bothered to implement with the pure HTML setup. With the complete refactor though, I figured it would be nice to have an automatically generating RSS feed. The issue is that =ox-rss=, the package used for generating RSS feeds only works on single files. The blog isn't one file though so we need to combine all the files into one and abuse sitemaps once again to create an RSS feed. I found [[https://writepermission.com/org-blogging-rss-feed.html][this]] blog post helpful in implementing the RSS feed. I ended up using the same configuration for it as [[https://nicolasknoebber.com/posts/blogging-with-emacs-and-org.html][this]] blog though because I found some functions in the first one didn't seem to work correctly anymore.
Finally, by using Org-Mode to generate the site, I was able to include by system configuration files in my site. I use Doom Emacs literate configuration to automatically generate all of my configuration files and since this is just a =.org= file, I can also compile that into HTML to easily display.
#+begin_src emacs-lisp
("org-config"
:base-directory "~/.doom.d/"
:base-extension "org"
:html-head "<link rel='stylesheet' href='/css/stylesheet.css' type='text/css'/>"
:html-preamble "<div class='topnav'><a href='/'>Home</a><a href='/projects.html'>Projects</a><a href='/blog'>Blog</a><a href='/config.html'>Dotfiles</a><a href='/about.html'>About Me</a><a href='/rss.xml'>RSS</a></div>"
:html-postamble nil
:publishing-directory "~/public_html"
:publishing-function org-html-publish-to-html
:headline-levels 4
:auto-preamble t)
#+end_src
Finally, I just publish it to HTML with
#+begin_src emacs-lisp
(org-publish-all t)
#+end_src
Although this was a little bit of a challenge to setup, I appreciate that I now effectively have my own highly-configurable static-site generator and no longer have to write all of this in raw HTML. It makes for a much more comfortable experience and should be stable enough that I shouldn't have to deal with debugging weird =elisp= issues for a while.
|