Dominik Süß

fighting computers since 1999

in

Using denote in hugo

My journey of adding denote support to the hugo static site generation tool


I recently stumbled upon Josh Beckman's wonderfull website and was very delighted by the way they use their website to share their notes. Combined with me reviving my telescope last week and needing a place to write things down that don't warrant a full blog post, I decided to build something like this as well.

I'm not a good notetaker but would like to become one. When trying out tools for this, I experimented with org-roam, obsidian and denote. Out of these tools, denote is the one I liked the most as it allows me to stay inside emacs, use org-mode to write the notes and is much simpler and faster than org-roam. The challenge then became - how can I get these notes published on my website?

The obvious first step here is to just add the denote note directory to my hugo content folder. Hugo has native support for org-mode so the pages built and I had a working first draft. It left two things to be desired though:

  1. Links don't work
  2. No way to display backlinks

Hugo is open source so I dug into the codebase and checked whether I could add support for both.

Supporting custom links

Hugo uses the go-org library to render content written in org mode. Alternatively I could export the org files to markdown first but I didn't want to rebuild the exports every time I changed a note.

When taking a look at how hugo converts org-mode to go, I saw that there are already customizations in place for the ReadFile function so I'll copy that approach. Link handling happens in the parseRegularLink function so instead of returning the value directly, I added a function called ResolveLink to the document config which allows for further customization in my PR.

The next question is: How should hugo resolve the links? Denote links are structured like this [[denote:<identifier>]]. The identifier exists in two places: The first part of the filename (before the -- delimiter) and inside of the the file. My first intuition was to iterate over all files in the same directory and check if they match the identifier. It turns out that this is quite tricky though as hugo also supports overlays and other ways to mess with the file system & content structure so I took another approach.

Since the identifier doesn't really change, I can just use it as the notes URL! That way, resolving links is just a matter of turning denote:20240610T082140 into ../20240610t082140/. The only downside to that is that now every note needs to contain the #+slug parameter but this is trivial to accomplish using the denote-org-front-matter variable.

  (setq denote-org-front-matter
"#+title:      %1$s
#+date:       %2$s
#+filetags:   %3$s
#+identifier: %4$s
#+slug:       %4$s
\n")

To resolve to the correct location when building the page, I prepared the changes for hugo which I'll post a PR for once the go-org PR is merged.

Backlinks

For the backlinks, the easy choice would be to use denotes dynamic blocks feature but that would require me to rebuild all dblocks on change and also prohibits linking from and to blog posts and other pages. I looked around and found a great article by Kaushal Modi that contains a snippet for dynamic backlink fragments using hugos partials. I customized it a bit to use a different regex recognizing org-mode links instead and also only linking based off the slug. This resulted in the following partial:

{{ $backlinks := slice }}
{{ $path_base := .page.Slug }}
{{ $path_base_re := printf `\[\[.+%s.+\]` $path_base }}

{{ if not (eq $path_base "") }}
  {{ range where site.AllPages "RelPermalink" "ne" .page.RelPermalink }}
    {{ if (findRE $path_base_re .RawContent 1) }}
        {{ $backlinks = $backlinks | append . }}
    {{ end }}
  {{ end }}
{{ end }}

{{ with $backlinks }}
    <section class="backlinks">
        {{ printf "%s" ($.heading | default "<h2>Backlinks</h2>") | safeHTML }}
        <nav>
            <ul>
                {{ range . }}
                    <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
                {{ end }}
            </ul>
        </nav>
    </section>
{{ end }}

Which works like a charm!

Compatibility mode

Now, I can't be bothered to build my page using a custom hugo version right now so the notes section will likely remain broken for a bit.

For people who have JS enabled, I've set up a workaround performing a link rewrite on the client side:

document.addEventListener("DOMContentLoaded", (event) => {
  document.querySelectorAll('a[href^="denote:"]').forEach(e =>
      e.href = '../' + e.href.toLowerCase().replace(/^denote:/,'') + '/'
  );
});

This works reasonably well but I want my website to be as accessible as possible so I'll still try to get the changes upstreamed.

If you want to check out the result for yourself, you should see a reference in this posts backlinks or in the top navigation menu.