myzettel

Table of Contents

Org Roam is the foundation on which i have implemented my Zettelkasten. This page documents my implementation besides tracking how the system evolved and provides necessary references.

Motivation

After reading an article about Roam Research, i tried it out. It was quite interesting. When searching for an open source alternative, i read a post in ycombinator, which led me to Org Roam. I was keen to avoid vendor lock-in and preferred text focussed tools. At that time, i was trying to use Emacs for my plain text accounting needs. Reading about Org Mode and GTD re-inforced the move to Org Roam. Later i also tested obsidian, the other popular alternative to Roam Research to get a feel of it.

Until the move to Org Mode, all my notes were in multiple formats like gdocs, docx, txt, odt, md etc.. They were scattered across a multitude of storage locations ranging from local hard disk to cloud services like GDrive and dropbox. Over a period of time, these notes became repetitive and disjointed, not easy to search and use/modify quickly. As a first step, i dumped all my technical documents from various formats to org format.

I read the concept note1 from the org-roam author and the book How To Take Smart Notes referred by him before proceeding further.

Process

To capture ideas or notes, orgzly or Emacs is to be used with Org Roam into a journal entry. At end of day or at the earliest, these journal notes are to be organized into permanent notes. I follow a single folder for maintaining all my notes.

From the beginning, the notes could be accessed through either Emacs or through a webpage, i.e the html served through a webserver inside my home. My agenda files were shared through Weddav, so i could have read and write access from mobile. A local webserver on the mobile could serve the synced site on the mobile, when i'm away from home. In early Mar'26, the web server moved to an oci instance. The content is currently synced between the oci instance, the desktop and the mobile using syncthing.

In late Sep'26, the content of the site was also made available in geminispace. Along with this change, the publishing pipeline moved from Hugo to org-publish, a org-mode native publishing tool. Now html/gmi format files are generated directly from org source files.

Current Implementation

A text first site

The site is lightweight in nature. The site content is available in both geminispace and www. For the conversion of content in org-mode to gemtext format, a forked version of ox-gemini package is used. The content is published in both the formats i.e html and gmi using the built in org-publish tool.

Both Notes and Journal files are available as Atom feed for html. The index page of gemini version is formatted in gemsub format.

Emacs config file is the single source of truth along with a css. Now, the content generation is driven entirely by emacs. The dependence on external tools/libraries have been removed, save for dart-sass.

Earlier Implementations

This section is meant to capture historical implementation details.

First version

My first version of Org Roam was entirely based on systemcrafters article2. This article allowed me to get started quickly using Ananke theme, which i followed up with the official org-roam configuration guide3. "org-id-get-create" shortcut was added from reading this4.

Later i removed the Ananke theme and went for a lightweight site which was usable in both mobile and desktop with simple navigation. I kept a single Javscript based search with hugo as the only dependency.

Git log of this implementation can be found here.

Second version

Around early Feb'26, I read a blog post titled "Visualizing Hugo blog posts with d3js" by Aris Bezas aka igoumeninja. I implemented a new hugo site with hugo-book theme and modified the Javascript provided by the author with generous help from claude and gemini to make it work with my theme and content.

The site supported all modern web features like dark mode support and mobile first UI. I tried to maintain the site lightweight by moving the javascript based features to load only when specifically opted by the user.

Around mid Mar'26, replaced the d3js library by sigma + forceatlas2 from graphology libraries with assistance from gemini. The decision to move was made keeping in mind the scalability of the new library and better graph quality.

After multiple iterations, forceatlas2 was replaced by FA2Layout async so as to benefit from the non-blocking UI. The site navigation was changed to show recent posts and featured posts. Graph and associated javascript still remained optional and available when opted by the website user. The earlier javascript based search was not implemented in this site. Graph view is disabled in mobile UI, as it is not very usable.

Git log of this implementation can be found here.

Third version

This third Hugo based version was retired on 2026-09-23.

ox-hugo is used to export the content from org format to markdown format. Hugo generates a static website in HTML format from the markdown formatted content.

On a quest to reduce dependencies as much as possible and to make future migration easier, i decided to use Hugo without theme.

The default site is lightweight in nature. Javascript is used minimally — for mobile navigation and on-demand features like the neighbour panel and graphical view. Dark mode is implemented entirely using CSS.

When the occasional js needs to be invoked, to ensure that text only browsers are not presented with non-functional buttons, the button definitions like below have been avoided.

<button
  id="neighbour-trigger"
  class="not-for-mobile not-for-graph-sidebar"
  data-lnk="{{ $lnk }}"
  data-ns-src="{{ $nsSrc }}"
  aria-label="Show neighbours in graph"
 >Neighbours</button>

Instead they are to be wrapped as follows:

<script>
 document.write('<button id="neighbour-trigger" class="not-for-mobile not-for-graph-sidebar" data-lnk="{{ $lnk }}" aria-label="Show neighbours in graph">Neighbours<\/button>');
                                                                                                                                                                                                         </script>

The relevant javascripts invariably begins with a guard:

const trigger = document.getElementById("neighbour-trigger");
if (!trigger) return;

The following scripts used in the site is available as literate programmes.

Git log for the hugo based implementation can be found here.

The initial graphical view in second version of the site used D3-force based javascript along with a script for mobile menu handling. A third Javascript was added for search integration by slightly modifying the one from first version. To reduce potential performance issues, an intermediate lightweight layer i.e neighbour panel has been added. It shows the neighbour information as a linked list grouped by Links and Backlinks using the neighbour-loader.js. The entry to the graphical view is now available in this neighbour panel.

I have used org-roam-UI mode as a benchmark for the graphical view and almost the entire code was generated with assistance from Claude and Gemini. While trying to reach feature parity with roam-UI, i ended up with a much larger codebase and a different library i.e used FA2 from graphology and sigma for rendering.

A python script gen_graph.py provides the necessary meta information about node and edge information in a json format. To further improve usability, the code related to ForceAtlas2 layout and Louvain community detection was moved from javascript/browser to this python script.

The entire graph rendering is done by graph.js using the json data referred above.

Except the graph page, the entire site is available in mobile view. Mobile view uses javascript mobile-menu.js to provide navigation.

The Hugo layout strictly adheres to the new template structure implemented in v0.146.0.

prabu@homepc2 /d/m/p/o/my_hugo_site (master)> tree layouts/
layouts/
├── _default
│   ├── _markup
│   │   └── render-link.html
│   ├── graph-map.html
│   ├── index.json
│   └── single.html
├── _partials
│   ├── date.html
│   ├── footer.html
│   ├── graph_scripts.html
│   ├── header.html
│   ├── meta.html
│   └── neighbour-panel.html
├── baseof.html
├── docs
│   ├── list.html
│   └── single.html
├── journal
│   ├── list.html
│   └── single.html
├── list.html
└── shortcodes
    ├── call_footer.html
    └── recent_notes.html

Initially used dropbox as cloud storage with org/Resources folder as base for the Zettelkasten system.

Dropbox_uploader script managed synchronization between desktop computers and Dropbox manually. I decided against using Dropbox desktop client, so that the process is as lightweight as possible.

I used dropbox so that the files are available for use with android apps like orgro and orgzly. After testing both, started using orgzly on android mobiles to sync with Dropbox.

As on 2024-03-18, instead of using Dropbox, decided to use webdav from rclone to sync with orgzly. This allowed both the desktop and mobile be in sync. Back links were still not supported in orgzly.

On 2024-03-19, checked out another tool named Markor. This tool is not as polished as orgzly, but it is more effective as it supports more file formats. Still backlinks are not working. This tool was removed on 2024-03-20.

Since no tool could handle bi-directional links in the org files natively on Android, decided to export the content as HTML. Installed rcx on 2024-03-19. Found it easy to sync the agenda files in org format using webdav between computers and Note6Pro.

Rclone tool in combination with rcx was used until Mar'26 to sync the above HTML content. Rcx also served as web server on the mobile.

References

Footnotes:

Date: 2024-03-31 Sun 19:48

Author: Prabu Anand Kalivaradhan

Created: 2026-10-11 Sun 21:08