Publish Org-Roam Notes as a Website
Table of Contents
Setting the Context
I love using emacs! I use it for the org mode. Org Mode is a mode for document editing, formatting, and organizing within the free software
text editor GNU Emacs and its derivatives, designed for notes, planning, and authoring, says a quick google search. Emacs has a package
named org-roam
which let's us create and interlink org documents. If you have ever used obsidian, then you know what I am talking about.
I wanted to publish my org-roam wiki as a website so I searched on the internet. I found various suggestions like using ox-hugo
or pandoc.
Someone created a separate program for it hyperorg. I didn't like any of these solutions, because emacs can export org files to html, and
doing so for a bunch of files in a folder shouldn't be that hard. Then I tried org-publish
, but it errored on export. It was not
identifying the org-roam links. Turns out that I have to run (org-roam-update-org-id-locations)
after creating new notes for emacs to know
about their locations on the filesystem.
Then there was another challenge, how could I publish some files, and not publish the rest, somewhat like drafts. There was no way of doing
this with org-roam, or org-publish. org-publish
does take a parameter :exclude "./exclude"
to exclude specified directory, but I had all my
roam notes in a single directory, so I couldn't use that. In the end I came up with something of my own.
How I made it work
First I added #+STATE: draft
in the org-document-info area of the org-roam files. Then I wrote a little function with the help of ChatGPT to
search for the files which don't have this in their document-info section. Once I had all those files, I published them using org-publish
putting the html files in the ./public
folder. I have all the attachments in ./assets
folder, and I just copy it over to the public folder
after html export. These are the rough steps.
- Add
#+STATE: draft
to the files which you don't want to publish. - Add
#+EXPORT_FILE_NAME: index.html
to the file you want to export asindex.html
- Run
(org-roam-update-org-id-locations)
. - Create
build.sh
andbuild-site.el
in the root of your org-roam notes directory - run
build.sh
It would be very annoying if I have to add #+STATE: draft
to every org-roam note that I create. Turns out we can write some elisp to
automate this. This code snippet below specifies the template for org-roam files. It adds #+AUTHOR:
, #+EXCLUDE_TAGS:
, and #+STATE:
. In the
index file, I have a lot of links to other notes, which I don't want to show yet, as they might be in WIP state. In such cases
#+EXCLUDE_TAGS:
comes in handy.
I added #+EXCLUDE_TAGS: noexport
on the top of my index file (which I export as index.html). Now whichever heading I want to hide, I just
have to add :noexport:
at the end of it, and all the content under it will be hidden/will not be there in the exported file. This way, I can
decide when is some note in a "ready-to-publish" state, and move it form a :noexport:
heading to a normal one.
(setq org-roam-capture-templates '(("d" "default" plain "%?" :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+TITLE: ${title}\n#+AUTHOR: Sahil Gautam\n#+EXCLUDE_TAGS: noexport\n#+STATE: draft\n"))))
build.sh
#!/bin/sh # remove timestamp cache if [ -d "$HOME/.org-timestamps" ]; then rm -rf "$HOME/.org-timestamps" fi # remove the html files, and build from scratch rm -rf ./public emacs -Q --script build-site.el # move all the assets to the public directory cp -r ./assets ./public
build-site.el
(require 'ox-publish) (defun my-org-publish-filtered-files () "Return a list of file paths to publish, excluding drafts." (let ((files (directory-files-recursively "." "\\.org$")) (filtered-files '())) (dolist (file files) (with-temp-buffer (insert-file-contents file) (unless (save-excursion (goto-char (point-min)) (re-search-forward "^#\\+STATE: draft" nil t)) (push file filtered-files)))) (nreverse filtered-files))) (defun my-org-publish-files (files) "Publish a list of files." (dolist (file files) (let ((org-publish-project-alist '(("my-org-site" :base-directory "." :publishing-directory "./public" :publishing-function org-html-publish-to-html :with-author nil ;; Don't include author name :with-creator t ;; Include Emacs and Org versions in footer :with-toc t ;; Include a table of contents :section-numbers nil ;; Don't include section numbers :time-stamp-file nil :recursive t)))) (setq org-html-validation-link nil org-html-postamble nil org-html-head-include-scripts nil) ;; Use our own styles (org-publish-file file)))) ;; Set up and publish (let ((files (my-org-publish-filtered-files))) (my-org-publish-files files))