Drew Haven a.k.a. Periodic

Adding notes to Blazestar.net


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. I started out by trying Obsidian, but I didn’t like the client and preferred to edit directly in NeoVim. I kept the overall layout and format. I do all my syncing with SyncThing.

I was inspired by EmaNote. 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 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.

interface Hierarchy extends Record<string, Hierarchy> {}

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.)

interface HierarchyNode {
  id: string | null;
  title: string | null;
  children: Hierarchy | null;
}
type Hierarchy = Record<string, HierarchyNode>;

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.

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, {});
}

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.

---
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 (
        <details open={pathname.startsWith(notePath) && "true"}>
          <summary>
            { node.id
              ? <a href={notePath} class={linkClasses}>{node.title || leader}</a>
              : <span class="header-only">{node.title || leader}</span>
            }
          </summary>
          <div class="child-notes">
            { <Astro.self prefix={notePath} hierarchy={node.children} /> }
          </div>
        </details>
      );
    }

    return (
      <div>
        <a href={notePath} class={linkClasses}>
          {node.title || leader}
        </a>
      </div>
      );
  })
}

<style>
.active {
  font-weight: bold;
}

.child-notes {
  margin-left: 1em;
}

summary {
  .header-only {
    color: var(--color-gray)
  }

  &:hover {
    color: var(--color-gold);
  }
}

</style>

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!