One of my favorite quotes of all time is this definition of insanity:
Insanity is doing the same thing again and again and expecting different results. It’s thinking - “this time it’s going to be different”.
I don’t know where the original quote comes from (Benjamin Franklin? Gandhi?) but I heard it for the first time from a villain in the game “Far Cry 3”. Here’s the clip if you want to watch it, I still get goosebumps from the voiceacting and motion capture: https://www.youtube.com/watch?v=rKMMCPeiQoc.
I used to be a prolific blogger. And then I somehow fell out of the habit of writing regularly, and instead got in the habit of tinkering with my blog.
There was a long break between each of these and the previous post. I did not write anything for a long time, realized that I hadn’t written anything for a long time, made a long blog post about how I’d made the blog better and easier to write, and then didn’t write anything else for another long stretch.
With that out of the way, let me tell you about how I’ve now made the blog much better and a lot less trouble to write…
I’m only half kidding. The blog’s backend is mostly unchanged - it still uses Hugo, it’s still hosted on Cloudflare Pages. I did try to use ox-hugo to export subtrees out of a single large org-mode
file into individual posts, but inter-post links via that method have apparently been broken for at least a year. I then opted to use the method outlined in https://yejun.dev/posts/blogging-using-denote-and-hugo/ with a few modifications to roll my own solution that exports from specially-marked Denote notes to Hugo markdown posts. The elisp code I hacked together with some help from https://claude.ai isn’t particularly well-written or generalizable, but it works well enough to do what I need it to do (mark a note as a blog post, export it, export all marked notes from directory) and stays out of my way otherwise. Here’s all of the code to make that happen:
;; Adapted from https://yejun.dev/posts/blogging-using-denote-and-hugo/
;; Converts denote links to hugo's relref shortcodes to generated files.
(advice-add 'denote-link-ol-export :around
(lambda (orig-fun link description format)
(if (and (eq format 'md)
(eq org-export-current-backend 'hugo))
(let* ((path (denote-get-path-by-id link))
(export-file-name (ameyp/denote-generate-hugo-export-file-name path)))
(format "[%s]({{< relref \"%s\" >}})"
description
export-file-name))
(funcall orig-fun link description format))))
;; Add advice around org-export-output-file-name so that I can generate the filename from denote frontmatter
;; rather than needing to add an explicit export_file_name property.
(advice-add 'org-export-output-file-name :around
(lambda (orig-fun extension &optional subtreep pub-dir)
(if (and (string-equal extension ".md")
(ameyp/denote-should-export-to-hugo))
(let ((base-name (concat
(ameyp/denote-generate-hugo-export-file-name (buffer-file-name))
extension)))
(cond
(pub-dir (concat (file-name-as-directory pub-dir)
(file-name-nondirectory base-name)))
(t base-name)))
(funcall orig-fun extension subtreep pub-dir))))
(defvar ameyp/denote--hugo-export-regexp "hugo_export[[:blank:]]*:[[:blank:]]*"
"The frontmatter property for indicating that the note should be exported to a hugo post.")
(defun ameyp/denote-generate-hugo-export-file-name (filename)
"Generates a hugo slug from the supplied filename."
(let* ((title (denote-retrieve-filename-title filename))
(date (denote--id-to-date (denote-retrieve-filename-identifier filename))))
(concat date "-" title)))
(defun ameyp/denote-should-export-to-hugo ()
"Check whether the current buffer should be exported to a published hugo post."
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(if (re-search-forward ameyp/denote--hugo-export-regexp nil t 1)
(progn
(let ((value (buffer-substring (point) (line-end-position))))
(or (string-equal value "t")
(string-equal value "true"))))))))
(defun ameyp/goto-last-consecutive-denote-property-line ()
"Move point to the last consecutive line at the beginning of the buffer that starts with '#+'"
(interactive)
(goto-char (point-min))
(let ((last-prop-line (point-min)))
(while (looking-at "^#+")
(setq last-prop-line (point))
(forward-line 1))
(goto-char last-prop-line)
(if (looking-at "^#+")
(beginning-of-line)
(message "No property line found"))))
(defun ameyp/org-hugo-mark-for-export()
"Inserts a frontmatter property to mark the denote file for export to a hugo post."
(interactive)
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(if (re-search-forward ameyp/denote--hugo-export-regexp nil t 1)
;; Found an existing property, set it to t.
(progn
(delete-region (point) (line-end-position))
(insert "t"))
;; No existing property found, go to the end of the frontmatter and insert the property.
(ameyp/goto-last-consecutive-denote-property-line)
(goto-char (line-end-position))
(insert "\n#+hugo_export: t")
)
))
)
(defun ameyp/org-hugo-export ()
"Export current buffer to a hugo post."
(interactive)
(if (ameyp/denote-should-export-to-hugo)
(let ((org-hugo-section "post")
(org-hugo-base-dir "~/Developer/wirywolf.com")
(org-hugo-front-matter-format "yaml"))
(org-hugo-export-wim-to-md))
(message (format "Not exporting %s" (buffer-file-name)))))
(defun ameyp/org-hugo-export-marked-files ()
"Export all marked files in dired buffer to hugo posts."
(interactive)
(let ((org-hugo-section "post")
(org-hugo-base-dir "~/Developer/wirywolf.com")
(org-hugo-front-matter-format "yaml"))
(save-window-excursion
(mapc (lambda (filename)
(find-file filename)
(ameyp/org-hugo-export))
(dired-get-marked-files))
)))
It mostly works, except if I need to embed hugo shortcodes inside a source code block, like in the above block. I haven’t figured out how to escape them automatically during the conversion process (I rely on ox-hugo’s markdown exporter) so for now, any hugo shortcodes must be manually escaped in the generated markdown as described in this post: https://liatas.com/posts/escaping-hugo-shortcodes/