From 609ac3e9c31da5f58ca6d1ea29145fe475f49c6a Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Tue, 21 Apr 2026 16:30:45 -0700 Subject: [PATCH] 2026-04-21 Post about adding notes hierarchy --- src/content/blog/2026-04-21-adding-notes.md | 161 ++++++++++++++++++++ src/lib/hierarchy.ts | 4 +- 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/content/blog/2026-04-21-adding-notes.md diff --git a/src/content/blog/2026-04-21-adding-notes.md b/src/content/blog/2026-04-21-adding-notes.md new file mode 100644 index 0000000..337935a --- /dev/null +++ b/src/content/blog/2026-04-21-adding-notes.md @@ -0,0 +1,161 @@ +--- +title: "Adding notes to Blazestar.net" +description: "I wanted to add some free-form notes to my site so that I could put up more permanent knowledge than chronological blog posts. I wanted to be able to use a system much like my markdown notes I keep for myself that had a nice menu like EmaNote. Here's how I made Astro do it for me." +pubDate: "Apr 21 2026" +tags: + - Coding +--- + +There are two influences that came together and prompted me to add some notes to this site. I've often wanted a quick way to add a page that holds some information or knowledge in a more structured way than just a blog, but I didn't want to have to manually build up the structure with each note. I wanted something that will take simple markdown files that I use for content and build a menu of notes that I can link to and readers can browse. + +The first is my notes system. A few years ago I wanted a good plain-text notes system to replace what I had been doing in [Notion](https://www.notion.com/). I started out by trying [Obsidian](https://obsidian.md/), but I didn't like the client and preferred to edit directly in [NeoVim](https://neovim.io/). I kept the overall layout and format. I do all my syncing with [SyncThing](https://syncthing.net/). + +I was inspired by [EmaNote](https://emanote.srid.ca/). I stumbled over it one time when I was researching a technical topic and was impressed by the layout and linking. It supports lots of ways to connect notes such as backlinks and "folgetzettel" links. I wanted something like that to organize my information. + +I experimented with migrating to EmaNote. I found the templates a little awkward to use, the documentation a little lacking and the migration was non-trivial. I really wanted to support the Haskell ecosystem, but I also want something that will work and get things done for me. I don't particularly like Astro as a project or a company, but it has been working for me, so I decided to stick with it. + +## Content Loading + +The first thing I needed was a content loader that would get all the notes and handle things like links between them. Fortunately, I found [astro-loader-obsidian](https://github.com/aitorllj93/astro-loader-obsidian) through the list of Astro plugins. I was a little worried at first that it didn't list anything about supporting folders or structure, which is something I really wanted. + +I installed it, made a dummy notes folder, configured it and then did good old `JSON.stringify` in my sidebar template to see what I got. Thankfully, it did support the full path to each note! Score! + +## Rebuilding the Note Structure + +So now that I had these notes, I needed to rebuild the structure so I could render a hierarchy. This was going to be a relatively simple matter of breaking of the paths and grouping them. I already know how to split paths and group things into a `Record`. This should be easy. + +I created a type like the following. Note that TypeScript does not support self-referential recursive types! It does allow self-referential interfaces though. Hence the odd syntax. + +```TypeScript +interface Hierarchy extends Record {} +``` + +However, after messing around with it for a while I realized I needed a little more information in my notes. I wanted the full ID/path and title of the note stored on each node. The ID would be an indicator of whether a given node in the tree has a note or not. That way I can support folder notes, such as having `foo.md` be the folder note where I also have `foo/bar.md`. The title lets me render the title without having to worry about making the file name be well capitalized or punctuated. + +This lead me to create the following data structure. Some nodes would have children, others not, and they may or may not have an ID or title. (I could write both of these as `type` declarations because they are _mutually recursive_ types. TypeScript is weird.) + +```TypeScript +interface HierarchyNode { + id: string | null; + title: string | null; + children: Hierarchy | null; +} +type Hierarchy = Record; +``` + +I started with algorithm of splitting all the paths into their parts, then going through and recursively grouping. However, I found this would be much simpler if I just did this as a fold/reduce so I could just focus on adding one path at a time. This gave me the following simple algorithm. Please ignore the repetition, it didn't seem worth cleaning up at the time. + +```TypeScript +function addToHierarchy( + tree: Hierarchy, + [noteId, title]: [string, string], +): Hierarchy { + const path = noteId.split("/"); + if (path.length === 0 || path[0] === "") { + return tree; + } + + if (!tree[path[0]]) { + tree[path[0]] = { id: null, title: null, children: null }; + } + let curr = tree[path[0]]; + + for (const node of path.slice(1)) { + curr.children ||= {}; + if (!curr.children[node]) { + curr.children[node] = { id: null, title: null, children: null }; + } + curr = curr.children[node]; + } + + curr.id = noteId; + curr.title = title; + return tree; +} + +export function makeHierarchy(notes: Array<[string, string]>): Hierarchy { + return notes.reduce(addToHierarchy, {}); +} +``` + +## Rendering Links + +So now I had to render the links. Astro thankfully _does_ support recursive components through `Astro.self`, so I was able to write a single component to render the hierarchy. + +```Astro +--- +import { type Hierarchy } from "../lib/hierarchy.ts"; + +interface Props { + prefix: string; + hierarchy: Hierarchy; +} + +const { prefix, hierarchy } = Astro.props; + +const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '/'); + +--- +{ + Object.entries(hierarchy).map(([leader, node]) => { + const notePath = `${prefix}/${leader}`; + const isActive = pathname === notePath; + const linkClasses = isActive ? "active" : ""; + + if (node.children) { + return ( +
+ + { node.id + ? {node.title || leader} + : {node.title || leader} + } + +
+ { } +
+
+ ); + } + + return ( +
+ + {node.title || leader} + +
+ ); + }) +} + + +``` + +And there we go, a list of links for my notes that mirrors the folder structure. + +## In Closing + +Overall it was a pleasant little project. I'm glad I now have a place to collect information online that I think might be useful to others as easily as importing a markdown file. I can even publish as simply as copying things directly from my main notes folder into the site with minimal changes. + +Please let me know if you have any thoughts or if it helped you. The best way to reach me is to ping me on Mastodon! + +Happy hacking! diff --git a/src/lib/hierarchy.ts b/src/lib/hierarchy.ts index 54fdc6f..b3016c5 100644 --- a/src/lib/hierarchy.ts +++ b/src/lib/hierarchy.ts @@ -3,14 +3,14 @@ export interface HierarchyNode { title: string | null; children: Hierarchy | null; } -export interface Hierarchy extends Record {} +type Hierarchy = Record; function addToHierarchy( tree: Hierarchy, [noteId, title]: [string, string], ): Hierarchy { const path = noteId.split("/"); - if (path.length === 0) { + if (path.length === 0 || path[0] === "") { return tree; }