Published: 14 Oct 2025 at 15:08 ADT
Go | Emacs
Go Emacs is a static blog site generator written in Go that leverages Emacs Org mode’s HTML exporting functionality. This is the most recent evolution of my Emacs CMS idea to use Org mode as a content management system for my blog. Go Emacs comes with the source code needed to generate a go-emacs binary that is used with the included go.emacs.el to generate blog posts.
The biggest issue I faced in the project was getting information from an Org mode document into the HTML for the website. In the previous iteration, I baked the metadata for a post directly into the HTML itself using a BEGIN_EXPORT html org source block. org-html-export-to-html will pick this up and insert the snippet into the produced HTML document. This worked well because it meant I only needed to send one HTML file to the server; however, there were drawbacks. For example, if I didn’t want to display parts of the snippet, I needed to remove them somehow on the server side. It was also cumbersome and ugly to have all the Org versions of my blog posts starting with a big block of HTML. For Go Emacs, I didn’t have to send files to the server because everything was taking place on my host machine.
After some research, I realized that the org-collect-keywords function could not only collect Org keywords, but could also extract arbitrary user-defined keywords. This felt like a good “Org native” way to store and collect post metadata, but the question remained: how to get the metadata from Elisp land into Go land?
After considering a few options, I decided to use TOML files to store the extracted keyword metadata and make it accessible to Go. TOML syntax is incredibly simple, and it is easy to deserialize on the Go end. Given that I was working with Emacs and Elisp, I considered trying to use Elisp itself to store configuration data, but I could not find a reasonable or reliable way to parse Elisp inside Go. It seemed like I would have to write my own package to parse Elisp inside Go, which felt outside the scope of what I was trying to accomplish. Similarly, I considered using some other Lisp dialect to keep the lispy emacsy feel going, but I ran into similar issues. As it stands, when a page or blog post is published with go-emacs-publish, a metadata.toml file is produced, which is then easily readable by the Go binary.
After creating this metadata.toml workflow, I realized that I could also use TOML to store CSS style information. Instead of expecting users to hand-edit the CSS files themselves, CSS styles are defined in a styles.toml file where the site colors and fonts can be easily configured. At runtime, this TOML file is then read by the Go binary to generate a vars.css file that holds all of the CSS variables available to configure. I could have simply allowed for the variables to be edited directly in a CSS file, but I thought it would make for a better user experience to restrict configuration to either Org tags or TOML files.
I wanted to add a searchable “tags” feature that allowed for posts to be sorted by tag or topic. I created an org keyword to store the tags for the post, for example #+tags: Emacs Go. In go-emacs-publish I then write those tags into the TOML file by collecting the tags with (org-collect-keywords '("TAGS")), which returns an alists of the keywords and their values. You can then extract the values with something like (car (alist-get "TAGS" keywords nil nil #'equal)). A quirk of alist-get is that it uses the eq function to test the keys to find the correct one to return, but eq only works on symbols and org-collect-keywords returns an alist where the keys are strings. To resolve this, you need to pass a TESTFN argument, such as #'equals or #'string= for this to work correctly. Once the tags are collected it can be inserted into the metadata file. The full function looks like this:
(defun go-emacs-publish-post-metadata () "Collecs metadata for a post and writes it to a metadata.toml file." (let* ((keywords (org-collect-keywords '("TITLE" "DATE" "TAGS" "SUMMARY"))) (title (car (alist-get "TITLE" keywords nil nil #'equal))) (date (car (alist-get "DATE" keywords nil nil #'equal))) (tags (car (alist-get "TAGS" keywords nil nil #'equal))) (summary (car (alist-get "SUMMARY" keywords nil nil #'equal)))) (with-temp-file "metadata.toml" (insert (format "title=\"%s\"\n" title)) (insert (format "tagString=\"%s\"\n" tags)) (insert (format "summary=\"%s\"\n" summary)) (insert (format "datestring=\"%s\"\n" date)))))
Finally, there isn’t much work to do on the Go side. The package I am using to deserialize the TOML file into Go is able to extract the keywords directly into a slice if the struct field you define to hold the data has a slice datatype. I used this same basic pipeline to get data from Emacs into go.
I have a few ideas for things to add next. For example, I can’t add custom HTML to the home page, or the footer. It would be nice to have a way to optionally add a snippet to the footer if there is something I want to appear at the bottom of every page. I would also like to do some incremental changes and improvements for the CSS and to make thumbnails optional for the homepage “list” mode.