Publish org-roam notes to personal wiki using ox-hugo and Quartz
Demo
The Notes section in this blog direct to my personal wiki site, which is built with Quartz from org-mode. Quartz is a static-site generator that transforms markdown files to a wiki-like site; some people also refer to it as digital garden. I have all my .org
notes with backlinks in org-mode, and use ox-hugo to export to hugo-flavoured markdown files, so that Quartz can render a functional website using its plugin.
Motivation
- I write all my notes in org-mode. However, it can be hard to read sometimes, especially when there are lots of Latex formulas.
- By publishing my notes, I can read them even when I’m away from my computer.
- Although there are plenty of publishing solutions for org-mode, Quartz is one of the best site generators with features that are great for digital garden, such as the graph view and hover page. Thanks to its OxHugoFlavouredMarkdown plugin, Quartz is compatible with the ox-hugo export format, so the setup is pretty much out-of-box.
Prerequisite
- A working Emacs configuration
- Org-roam basic setup
Org-mode itself is awesome for managing todo and notes. At some point, I discovered org-roam, a package that’s inspired by Roam Research. Combining the powerful org-mode and org-roam, it becomes my knowledge management tool to quickly jot down notes and gradually build a knowledge base of my own.
I won’t spend too much time on this topic because emacs is a rabbit hole. Check System Crafters’ Build a Second Brain in Emacs with Org Roam tutorial for more information about org-roam setup.
Install and set up Quartz
Initialize site with Quartz
Quartz is extremely easy to set up, even if you aren’t familiar with hugo or any other static site generator. Following the getting started guide, the initialization only takes few minutes and it’s already a nice-looking out-of-box digital garden site.
During the initialization, it asked for content info and I chose Empty quartz
for a fresh site. It also asked how it should handle links and I chose the shortest path.
◇ Choose how to initialize the content in `/path/of/quartz/content`
│ Empty Quartz
◇ Choose how Quartz should resolve links in your content. You can change this later in
`quartz.config.ts`.
│ Treat links as shortest path
After the initialization, I ran npx quartz build --serve
to preview. The site is ready, and I just need to put notes in markdown format in content/
folder.
Directory structure and config files
├── content
│ ├── braindump
│ ├── notes
│ └── index.md
├── ...
├── quartz.config.ts
└── quartz.layout.ts
Similar to hugo, Quartz also uses content/
folder to store content files of the site, where the index.md
is the landing page of Quartz.
Quartz supports nested folders and will display them in Explorer and Folder Listings, and I put braindump
and notes
as subfolders to separate my personal and study notes under content/
.
quartz.config.ts
and quartz.layout.ts
are the two configuration files, in which quartz.config.ts
defines the website’s basic settings such as plugins, site url, website title, etc., while quartz.layout.ts
controls the site’s layout and style. The variables are quite self-explainatory, and Quartz’s configuration guide is everything you need to complete the setup.
To enable its compatibility with the hugo-flavoured markdown exported from org-mode, simply add Plugin.OxHugoFlavouredMarkdown
to the quartz.config.ts
file:
: {
plugins: [
transformers// ...
Plugin.OxHugoFlavouredMarkdown(),
Plugin.GitHubFlavoredMarkdown(),
// ...
,
], }
Hosting and deployment
Quartz provides guide on setting up Github repository and hosting from different hosting providers. I use Cloudflare Pages, but any other provides that can deploy static HTML should work.
Org-roam and ox-hugo setup
To publish org-roam notes, I use ox-hugo’s one post per org file workflow to export markdown for each org file. Ox-hugo prefers one post per org subtree, but it’s not the way org-roam works. Anyways, this one post per org file workflow works for me and I didn’t find any functionality it’s lacking.
Front matter in org-roam notes
Quartz and hugo shares the same front matter, and because ox-hugo support hugo’s front matter, it also supports Quartz’s without any modification. When I take notes in org-mode, I use templates to add the metadata based on ox-hugo’s documentation, and ox-hugo will convert those metadata to the front matter when exporting org to markdown files.
For the front matter in the note level,I try to keep it simple (additional configurations will be covered in folder level, see below):
Org-roam notes’ front matter
#+filetags
: the tags of the note in the Quartz site#+hugo_section
: the sub folder of Quartz’scontent/
directory#+date
: self-explanatory#+hugo_lastmod
: define the last modified date, which is the preferred date I set in myquartz.config.ts
Ox-hugo export config
Based on ox-hugo’s guide about auto-saving, I created a .dir-locals.el
file in my org-roam-directory
. These settings will be applied to all files under the selected folder.
"1-main/"
((
. ((org-mode . (eval . (org-hugo-auto-export-mode))
("/path/to/digital-garden")
(org-hugo-base-dir . "yaml")
(org-hugo-front-matter-format .
)) )))
1-main/
is the folder where the settings will be applied:
org-hugo-auto-export-mode
: ox-hugo’s auto-saving functionorg-hugo-base-dir
: in this case, it’s the Quartz directoryorg-hugo-front-matter-format
: set the front matter to yaml
Exclude notes from export
To exclude notes that are not to be exported by org-hugo-auto-export-mode
, we can define it by adding a special tag #+hugo_tags: noexport
to these notes. In my org-roam-capture-templates
, I have #+hugo_tags: noexport
in front matter for all of my notes. If I want to publish the note, I will remove this tag or comment the line, and ox-hugo will pick it up and export it to markdown.
setq org-roam-capture-templates
("o" "moc" plain
'(("\n*Link*: %?\n\n"
"1-main/${slug}.org" "#+title: ${title}\n#+filetags: :moc:\n#+hugo_section: braindump\n#+date: %u\n#+hugo_lastmod: %u\n#+hugo_tags: noexport\n")
:if-new (file+head t
:immediate-finish t
:unnarrowed 1))) :empty-lines-after
If there’s a specific subtree I do not want to export, I can also exclude it by tagging with :noexport:
.
Auto-update timestamp for org-roam notes on save
I also include a function in my Emacs config so that it automatically updates #+hugo_lastmod
whenever I press save. Of course, I can also use ox-hugo’s #+hugo_auto_set_lastmod: t
, but I prefer to have the date shown within my org files.
;; Update last modified date for ox-hugo export
(after! orgsetq time-stamp-active t
("#\\+hugo_lastmod:[ \t]*"
time-stamp-start "$"
time-stamp-end "\[%Y-%m-%d\]")
time-stamp-format 'before-save-hook 'time-stamp)) (add-hook
My note-taking and publishing workflow
My publishing workflow is extremely simple:
- When I create a new org-roam note, it pops up with front matter templates.
- Once I finish writing, if I want to publish the note, I remove the
#+hugo_tags: noexport
tag. - When I hit save, Emacs automatically updates the
#+hugo_lastmod
to the current date. Meanwhile, it will convert.org
to.md
and show a message in the echo area, indicating the location of.md
file. - Once in a while, I go to the Quartz directory and use
magit
to push changes to Github. I could automate this step by setting a cron job, but I prefer to have a bit more control in case I accidentally upload my private notes.
Limitations
Callout formatting
Because I’m writing in org, I won’t be able to use some nice features that are packaged with Quartz such as the Obsidian-style callout. A workaround is to insert #+begin_export md
snippet to show the callout box in the published site:
#+begin_export md> [!note] An intuitive way to think about why it makes more sense.
> The distribution that are closer to normal distribution usually makes more sense. In this example, the distribution of group C and of D in alternative hypothesis are more closer to normal distribution (compare the data points vs the mean)
#+end_export
Output:
Image handling between org-mode and Quartz
For image linking, it becomes a bit tricky. Ox-hugo will copy images to static/
folder, which Quartz doesn’t use and will be emitted. I didn’t find the option to change the image output directory in ox-hugo, so I ended up manually copying all my images from static/ox-hugo
to content/ox-hugo
when I push the changes.
Speed of global graph
In Quartz, the local graph looks decent, but the global graph gets significantly slower with the increasing number of notes. I do not use the global graph in Quartz (I prefer org roam ui mode
that comes with org-roam) so it’s not an issue for me. And if I have to, I can always refer to Hugo Cisneros’ blog to build one from scratch from the org-roam db.