Making Tag Pages in an Org Mode Blog
Dec 30, 2024
I have finished converting the old Hugo blog to the new org-publish format. It was quite a bit of work, but I tried to automate as much as possible. I wrote about the initial setup here. One minor change has been to remove the “home” and “up” links that org-publish inserts at the upper-right. This just requires a simple change to the org-publish-project-alist
:
:html-link-home "" :html-link-up ""
Just make sure to change them wherever they occur in the alist.
A bigger issue was displaying post tags and creating tag pages. Tag and category pages in Hugo were fairly simple. Every time the site was built, Hugo would create the tag pages reflecting any new tags. Unfortunately, I could find no way to automatically do this with org-publish. My first thought was to simply forego tagging since there is a blog archive page with links to every post. A search on that page can find whatever you need, if you happened to know what you needed to search for. It’s not a good way to simply browse an area of interest, however.
On the other hand, making tags manually was never going to work for me either. That would require manually adding tags to the post, being sure to use the same names for tags across posts. Otherwise, I’d end up with some posts tagged “org” and others tagged “org-mode”. Then, I’d have to create a new tag page if needed, make a link from the post to the tag page, then another link from the tag page back to the tagged post. There’s no chance I could, or would, consistently make that happen. Fortunately, though, because this is Emacs, everything is scriptable.
Keep in mind that I’m a philosopher, not a coder. Here are my steps to solving a problem:
- Figure out what I want.
- Find someone who has posted something close to what I want.
- Make changes to their code to make it do what I want.
- Keep hacking at it until it works.
- Realize that what I thought I wanted wasn’t really what I wanted, so return to step 1 and repeat until satisfied.
So, the result is likely to be sloppy and inefficient, but it generally works in the end. I started with this post from Christian Tietze, who solved my first problem of consistently using tags across the site. From him, I got these three functions:
(defun orgblog-all-tag-lines () "Get filetag lines from all posts." (let ((post-dir orgblog-posts-directory) (regex "^#\\+filetags:\\s([a-zA-Z]+)")) (shell-command-to-string (concat "rg --context 0 --no-filename --no-heading --replace \"\\$1\" -- " (shell-quote-argument regex) " " post-dir)))) (defun orgblog-all-tags () "Return a list of unique tags from all posts." (delete-dups (split-string (orgblog-all-tag-lines) nil t))) (defun orgblog-select-tag () "Select and insert a tag from tags in the blog." (defvar newtag) (setq newtag (completing-read "Tag: " (orgblog-all-tags))))
The first uses ripgrep
to get all of the filetag lines from the headers in the posts directory. The second splits those lines into a list of separate words and deletes the duplicates. The third displays the list and allows the user to select a tag to be assigned to a variable. The only things I believe I’ve changed here are variable names. I also explicitly search the posts directory rather than the current directory since I write posts initially in a drafts directory. Since the tags are separated by a space in each filetag line, I can use the default value for split-string
. If the desired tag is not on the list, just hit return
and the new tag gets assigned to the variable.
At this point, I had to venture on my own into unknown waters. First, I needed to insert the new tag into the post. There’s no real reason to keep the filetag lines, except that I might find a better way to use them in the future. So, I add the new tag to the end of the filetag line. I need to display it in the published post, though, so each post has a line at the end1 that begins with “Tagged:”. This function handles that:
(defun insert-post-tag () (orgblog-select-tag) (beginning-of-buffer) (search-forward "#+filetags" nil 1) (end-of-line) (insert (concat " " newtag)) (beginning-of-buffer) (search-forward "Tagged:") (end-of-line) (insert (concat " [[file:../tags/" newtag ".org][" (s-titleized-words newtag) "]]")))
Note that I use Sveen Magnars’ s.el library to capitalize the tag names for display. The next thing to do is to add the tagged post to the tag page. To do that, I use this function:
(defun add-post-to-tagfile () (defvar tagfile) (defvar post-filename) (defvar post-title) (setq tagfile (concat "../tags/" newtag ".org")) (setq post-filename (f-filename (f-this-file))) (progn (beginning-of-buffer) (search-forward "#+title: " nil 1) (setq post-title (buffer-substring (point) (line-end-position)))) (when (not (file-exists-p tagfile)) (f-append-text (concat "#+title: Tagged: " (s-titleized-words newtag) "\n#+setupfile: ../org-templates/post.org\n") 'utf-8 tagfile)) (f-append-text (concat "\n- [[file:../posts/" post-filename "][" post-title "]]") 'utf-8 tagfile))
To create the links, we’ll need the filename and title of the post. We’ll also need the filename for the tag page. Note that this function uses both the s.el and Johan Andersson’s f.el libraries. In short, here’s what the function does:
- Create the filename for the tag page in the tags directory.
- Get the filename for the post.
- Extract the post title from header title-line.
- Create the file for the tag page if necessary.
- Add a link to the post at the bottom of the tag page.
The final thing is to put all of functions together into one interactive function:
(defun orgblog-add-tag () (interactive) (orgblog-select-tag) (insert-post-tag) (add-post-to-tagfile) (save-buffer))
The only thing left to do manually is to add a new tag page to the index page of the tags directory. I’m sure I could automate that with some effort, but at this stage I hardly ever use any new tags, so I’m not convinced that it would be worth the effort.
The only minor glitch in the system is that, for some reason, I have to press return
twice to select the tag. I’m not sure why. Eventually it will bother me enough to figure it out.
Footnotes
I did discover that it needs to be before the footnote section, or it won’t be displayed.