In the first part of this series I’ve outlined the main factors for moving my digital garden / braindump / Zettelkasten to org-roam and which factors have facilitated this decision. In the 2nd part I will expand more how I’ve built the new brainfck.org usinghugo, ox-hugo and org-roam.
Extracting tiddlers from my Tiddlywiki setup was only the first step towards a Second Brain using org-roam. Since I’m a clear advocate for public digital gardens, I didn’t want to keep my notes only for my self. Having built several sites with hugo already, it felt natural to chose it as a publishing system for my new setup.
In the following I will try to emphasize some important challenges I have experienced while migrating Tiddlywiki tiddlers to org-roam, creating and editing the content and finally*export* it to HTML via hugo
.
hugo
As a starting point I have used Jethro’s braindump repository especially for the Elisp
part. First of all I’m a big fan of Makefiles
:
I use python
for the export task:
This small snippet generates ❶ build statements for ninja ❻. The build.ninja
file will contain something similar to:
For each folder in my org-roam-directory
(pages ❷, books ❸, journal ❹, topics ❺)
- glob will find any ORG files
- a new ninja build statement will be written to
build.ninja
Each build command consists of org2md
which internally calls publish.el
:
The main function brainfck/publish
❶ basically calls org-hugo-export-wim-to-md
❷ which will “export the current subtree/all subtrees/current file to a Hugo post”. Before doing so some local variables are set and org-id-extra-files
is populated with all available ORG roam file paths. This variable holds all files/paths where ORG should search for IDs.
And because some IDs couldn’t be resolved properly I had to use some “hook” ❹ for rewriting ❸ some file paths within the generated markdown files.
For testing purposes you can call the publish.el
with just one argument:
Backlinks
Backlinks are an essential feature that let you visualize inter-connected content. Whenever I set a link to another org-roam
node in an ORG file, the exported markdown content will look like this:
You can see I’ve set a reference to SSH which looks like this:
[SSH]({{< relref "../topics/ssh.md" >}})
The question is: For a given node/topic how can we find all nodes containing a link to current node? Well we can parse content and actually search for that specific topic. In hugo
you can do something like this:
- ❶
.page.File.LogicalName
is sth likessh.md
-
["/(]%s.+["/)] .page.File.LogicalName | lower
will then yield["/(]ssh.md.+["/)]
-
- ❷ find any lines containing the logical file name (
ssh.md
) inside parantheses- examples: [ssh.md], “ssh.md”, (ssh.md)
- ❸ if we have any matches add page to
$backlinks
slice
Let’s have a look at the regular expression. Therefore I’ll use some Go
snippets to test the regexp:
[/ssh.md" */>}})]
[/ssh.md" */>}})]
[/ssh.md" */>}})]
Once we have populated the backlinks
slice with a list of pages backlinking to the current page we can then search inside the page content for exactly the lines containing the backlink:
{{ $content_re := printf `.*\[%s\].*` .page.Title }} ❶
...
{{ range $backlinks }}
{{ $matches := findRE $content_re .RawContent}}
<li class="lh-copy"><a class="link f5" href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ if $matches }} ❷
<blockquote>
{{ range $matches }}
{{ . | markdownify }}
{{ end }}
</blockquote>
{{ end }}
{{ end }}
...
- We search for any line containing the current page title (❶)
- If we have any matches we call
markdownify
against that line (❷)
And this is how the result looks like:
For the sake of completeness here’s the full backlinks partial:
As a last step I had to make use of this partial in my single.html template:
Section pages
Group topics by capital letter
For the topics page I wanted to group my topics by the first letter. Therefore in layouts/topics/list.html
I’ve inserted following:
Group books by year and month
Following snippet will show a list of books grouped by year. For each year each book will be shown along with the date
in yyyy-mm
format.
{{ define "main" }}
<main class="center mv4 content-width ph3">
<h1 class="f2 fw6 heading-font">{{ .Title }}</h1>
{{ .Content }} {{ range (where .Site.RegularPages "Type" "in" (slice
"books")).GroupByDate "2006" }}
<h2>{{ .Key }}</h2>
<ul class="list-pages">
{{ range .Pages.ByDate }}
<li class="lh-copy">
{{ $curDate := .Date.Format (.Site.Params.dateFormat | default "2006-02" )
}}
<span class="date">{{ printf "%s " (slicestr $curDate 0 7 ) }}</span>
<a class="title" href="{{ .Params.externalLink | default .RelPermalink }}"
>{{ .Title }}</a
>
</li>
{{- end -}}
</ul>
{{ end }}
</main>
<div class="pagination tc db fixed-l bottom-2-l right-2-l mb3 mb0-l">
{{ partial "back-to-top.html" . }}
</div>
{{ end }}
org-roam
As a complete
org-roam
novice I’ve found Getting Started with Org Roam - Build a Second Brain in Emacs (notes) to be a quite good introduction. It will give you enough background to get you started withorg-roam
. For more advanced topics you could also read 5 Org Roam Hacks for Better Productivity in Emacs or check out my org-roam topic for more resources.
By default all org-roam nodes are placed within the same directory. However, one big directory for all notes didn’t resonate with me at all. I came up with following hierarchy inside org-roam-directory
:
org/ This is the root org-roam directory.
-
books/
- this is where all books (stored as individual ORG files) should be located at
- I consider these files my literature notes
- thoughts and concepts found within one book may remain here
- quotes are now stored in the same (book ORG mode) file (example)
-
topics/
- all individual topics are stored here
- Examples: SSH, DDD, Attention Economy
- I don’t distinguish between collection nodes, thoughts and concepts
-
journal/
- files inside this folder are daily journals
- each file name has following format:
YYYY-MM-DD.org
- this is where I usually store thoughts, links which I haven’t categorized yet
- or put into the right topic
- notes/
Capture templates
For rapid capture org-roam
uses pre-defined capture templates
You can also store templates in Org files.
(similar to ORG mode capture templates) whenever a new entry (topic, book, note, quote etc.) should be added. These are mine:
Per default ❶ every new entry is a topic. Additionally I want every journal ❷ file to contain several meta information (properties) (like #+date
and #+filetags
).
Last but not least I want every book ❸ to be stored under <ORG Roam directory root>/books/
.
Emacs Kung Fu
As I was transitioning content from multiple folders into the org-roam
directory I’ve used Emacs editing capabilities to edit and create content using small Elisp
snippets and macros. Let’s explore some workflows.
Insert content at point
Whenever I was adding content (e.g. from sub-tiddlers) to main topic nodes (previsouly main tiddler in Tiddlywiki), I wanted to quickly jump between directories where my tiddlers were exported as org content.
(defun dorneanu/roam-insert (dir) ❶
(let* (
(filename (read-file-name "filename: " dir nil nil nil)))
(insert-file-contents filename))
)
;; Define global key bindings ❷
(global-set-key (kbd "C-c m b") (lambda () (interactive) (dorneanu/roam-insert "/cs/priv/repos/brainfck.org/tw5/output/books")))
(global-set-key (kbd "C-c m t") (lambda () (interactive) (dorneanu/roam-insert "/cs/priv/repos/tiddlywiki-migrator/org_tiddlers")))
(global-set-key (kbd "C-c m .") (lambda () (interactive) (dorneanu/roam-insert "/cs/priv/repos/roam/org/topics/")))
Therefore I’ve defined a function ❶ which reads a file content after this has been selected. The (temporary) key bindings ❷ allowed me to jump between following folders and insert content quickly:
-
/cs/priv/repos/brainfck.org/tw5/output/books
- This is where I’ve exported my book tiddlers along with their correspondig sub-tiddlers (read the first post for the explanations regarding books and their sub-tiddlers)
-
/cs/priv/repos/tiddlywiki-migrator/org_tiddlers
- This is where all tiddlers got exported to initially
-
/cs/priv/repos/roam/org/topics
- this is the root org-roam folder for topics
Add structure template for quotes
Let’s say you have following ORG content:
* Book title
** Notes
*** Note 1
Some text
*** Note 2
Another text
*** Note 3
Some loooooong text
How can you easily put the content underneath each note (Note 1, Note 2, Note 3) into quote blocks? Here is where macros came to my rescue. With my cursor on Note 1
I typed:
-
C-x (
kmacro-start-macro
- start macro
-
g j
outline-forward-same-level
- go to next headline (in the same level)
-
j
(move cursor to next line) -
M-m i p
mark-paragraph
- mark whole paragraph
-
C-c C-,
org-insert-structure-template
- wrap marked region into …
-
q
- a quote block
-
C-x )
kmacro-end-macro
- end macro sequence
Here is some screencast:
Conclusion
In retrospect I think I’ve spent way to much pretious lifetime for this project - and I’m not finished yet. There are still to many empty topics (no content at all) and links pointing to nirvana (e.g. links in old Tiddlywiki syntax). However, I think, the effort will pay off in the long run! In fact I already feel more productive as I’m able to quickly search for notes (in books, topics, journals etc.) and create these on-the-fly if not existant.
I’ve definitely improved my Emacs Kung Fu™ and learned even more about its editing features (macros!). I also hope org-roam
will help me produce even more content and prevent me from just collecting random notes.
Top comments (0)