Luhmann turns a directory of AsciiDoc files into a personal Zettelkasten β a linked web of notes β served over plain HTTP on localhost. You write notes in your own editor; Luhmann renders them, connects them, indexes them, and keeps the browser in sync as you work.
This is a reimplementation in the Blaise Pascal language, built with PasBuild and structured as a hexagonal (ports and adapters) application. Over the original Clojure version it adds automatic backlinks, tag-based organisation, an interactive note graph, a broken-links report, and a JSON API for editor integration.
- Notes and rendering
-
-
Renders each
.adocnote to HTML via theasciidoctorbinary and serves it wrapped in a clean, consistent UI. -
Parallel build β renders notes concurrently at start-up, so even a large Zettelkasten is ready in a second or two (see Startup performance).
-
- Connections
-
-
Automatic backlinks β you only ever write forward links (
xref:); Luhmann computes the reverse direction and shows, on every note, which other notes link to it. No manual maintenance. -
Interactive note graph (
/graph) β an Obsidian-style force-directed map of your notes. Click a node to open that note; double-click to re-centre the graph on it; the browser Back button returns you to the graph. -
Broken-links report (
/broken) β lists every cross-reference whose target note does not exist, so dangling links are easy to find and fix.
-
- Organisation and search
-
-
Tags β organise notes with an in-note
:tags:attribute rather than by folder; untagged notes fall under a configurable catch-all label. Browse all tags at/tags. -
Full-text search β a built-in index over every noteβs title and body, with prefix matching (and
*for substring) and highlighted result snippets.
-
- Workflow
-
-
Live reload β edit a note in any editor and save; the browser refreshes itself automatically over a WebSocket. A page also re-syncs when you switch back to its tab, so listing pages never go stale.
-
Launch your editor from the browser β press
eon any note (or call the edit API) and Luhmann opens that note in your configured editor, GUI or terminal. See editor integration. -
Keyboard shortcuts β
/to search,eto edit,gto open the graph for the current note (see Keyboard shortcuts (in the browser)).
-
- Integration
-
-
Built-in JSON REST API β query backlinks, the link graph, and tags, and trigger an editor launch, from your own tools or an editor plugin. See REST API.
-
- Local-first and self-contained
-
-
Runs as a single process on
localhost, no database, no cloud, no account. -
The graph library is vendored, so the graph works fully offline.
-
No external services beyond the
asciidoctorbinary used for rendering.
-
All endpoints are GET and take their input from query parameters. The graph
and tag endpoints return JSON; the rest return HTML pages.
| Endpoint | Returns | Description |
|---|---|---|
|
HTML |
A rendered note page (with chrome, tags and backlinks). |
|
HTML |
Full-text search results page. |
|
HTML |
The interactive graph page (optionally |
|
HTML |
All tags with counts, or the notes carrying one tag. |
|
HTML |
The broken cross-references report. |
|
JSON |
Notes that link to |
|
JSON |
The neighbourhood subgraph around |
|
JSON |
The whole-corpus graph (nodes + edges). |
|
JSON |
Every tag with its note count: |
|
text |
Launch the configured editor on |
|
WebSocket |
Live-reload channel; the server pushes a |
A note id is a path relative to the notes directory, e.g.
fruit/apple.adoc.
Luhmann is a hexagonal (ports and adapters) application. A framework-free
domain core β the link graph, tag index, change orchestration and AsciiDoc
extractors β depends only on a set of port interfaces and knows nothing of
HTTP, the filesystem or asciidoctor. Adapters implement those ports with
concrete technology, and the composition root wires them together. This keeps
the core fast to test (in-memory, no infrastructure) and every technology
choice swappable.
For the full picture β the ports and adapters, the unit map, how backlinks are maintained, and why this shape β see the architecture guide. The original design rationale and Blaise-specific implementation notes are in the design document.
Prerequisites:
-
A Blaise compiler binary (
v0.12.0or newer). -
PasBuild on the
PATH. -
The
asciidoctorbinary (/usr/bin/asciidoctor) for rendering, andfindfor note enumeration.
pasbuild compile --compiler /path/to/blaiseThe executable is produced at target/luhmann, with static assets copied to
target/public.
target/luhmann /path/to/notes/path/to/notes is a directory of .adoc files with an index.adoc entry
point. Open http://localhost:2022 to view the Zettelkasten.
The static-asset directory is resolved as: $LUHMANN_PUBLIC, else public
beside the binary (the PasBuild layout), else src/main/resources/public.
The repository ships with a small example Zettelkasten under example/ β a
handful of interlinked notes about fruit. Point Luhmann at it to see the
features in action:
target/luhmann exampleThen open http://localhost:2022 and explore:
-
the Backlinks panel on every note β e.g. open xref-rich
fruitand see the five notes that reference it; -
the graph (top-right Graph link) showing the notes clustering around the shared concepts (
fruit,pome,citrus); -
tags β every note is tagged, and nothing is organised by folders;
-
search β try
app,*range, orcitrus.
It is deliberately tiny, but it demonstrates the whole idea: atomic notes, organised by tags and links rather than folders, with structure emerging from the connections between them.
| Key | Action |
|---|---|
|
focus the search box |
|
launch the configured editor on the current note |
|
open the graph centred on the current note |
|
unfocus the search box (so the single-key shortcuts work again) |
Luhmann runs with sensible defaults; everything is optional. To override a
setting, create a .luhmann.ini file inside your notes directory (the same
directory you pass on the command line):
# ~/Documents/second-brain/.luhmann.ini
port = 2022
render-jobs = 12
editor = gnome-terminal -- mceditAvailable keys cover the port, the number of parallel render jobs, the editor
command (GUI or terminal), the catch-all tag label, and the asciidoctor path.
At startup Luhmann prints which config file it loaded so you can confirm it was
picked up.
See the configuration guide for the full list of
settings with defaults, and for editor-integration details (GUI vs terminal
editors, and how to launch console editors such as vim or mcedit).
Declare tags inside a note with the AsciiDoc :tags: attribute:
= Apple
:tags: fruit, pome, edible
An apple is a pome fruit ...Notes with no tags are filed under the catch-all label (default unfiled), so
every note is reachable from /tags.
The search box (or /search?q=β¦β) runs a full-text query over every noteβs
title and body. Matching is case-insensitive and works on word prefixes by
default, so a partial word finds the whole word:
| Query | Matches |
|---|---|
|
any note containing a word starting with |
|
the same note (a full word is a prefix of itself). |
|
notes containing a word starting with |
To match a fragment in the middle or end of a word, prefix the term with *
(a substring search):
| Query | Matches |
|---|---|
|
nothing β |
|
Zettelkasten β the fragment appears inside the word. |
Prefix is the default because it keeps short queries focused; reach for *
when you specifically want an infix match.
At start-up Luhmann renders every note once by shelling out to asciidoctor.
That cost is dominated by process-startup latency (~150 ms per note), so the
build renders notes in parallel: up to render-jobs asciidoctor processes
run at a time, and the result is identical to a serial render.
Measured on an 86-note corpus (16-core machine):
render-jobs |
Build time | Speedup |
|---|---|---|
1 (serial) |
~13.0 s |
1.0x |
8 (default) |
~3.0 s |
4.3x |
12 |
~2.3 s |
5.6x |
16 |
~2.1 s |
6.3x |
Set render-jobs to roughly your physical core count. Beyond that the gains
flatten (the remaining time is per-process startup plus the serial indexing
phase). The build prints its own timing on the Build complete line.