My previous approach to displaying my notes on this site was to render them as a tree of infinitely nested topics and subtopics:
Parents and children
To achieve that nesting, my solution was to manually give each note that represented a subtopic (or subsubsubtopic) a parent
value (via markdown frontmatter) and then move any child notes (i.e. notes with a parent
value) into their parent’s children
array:
/**
* Given an array of collection items, returns the array with child items nested under their parents.
*/
function nestChildren(collection: Note[]): Note[] {
// Step 1: Create a mapping of item IDs to item data
const slugToNodeMap = collection.reduce(
(nodesBySlug, item): Record<string, Note> => {
// Add an empty children array to the item's existing data
nodesBySlug[item.id.toLowerCase()] = { ...item, data: { ...item.data, children: [] } }
return nodesBySlug
},
{} as Record<string, Note>,
)
// Step 2: Build the nested item tree
const tree = collection.reduce((roots, item): Note[] => {
// Find the node matching the current collection item
const node = slugToNodeMap[item.id.toLowerCase()]
if (item.data.parent) {
// If the note has a parent...
const parentNode = slugToNodeMap[item.data.parent.toLowerCase()]
if (parentNode) {
// ...add the item's data to the parent's children array
parentNode.data.children.push(node)
} else {
console.error(`Parent slug "${item.data.parent}" not found (this should never happen).`)
}
} else {
// If the item has no parent, treat it as a new root-level note
roots.push(node)
}
// Return the updated tree and keep iterating
return roots
}, [] as Note[])
// Return the final, nested tree
return tree
}
/**
* Returns all notes with child notes nested under their parents.
*/
export const getNestedNotes = async (): Promise<Note[]> =>
nestChildren(await getCollection('writing', note => isNote(note)))
Incremental left padding
My solution for visually indicating how those notes related to each other was to multiply each note’s left-padding
by 1.4x
its descendent level:
<ul class="list-notes">
{
notes.map(note => {
const getLinkText = (item: Note): string => item.data.title || item.id
const getChildren = (item: Note, level: number) =>
item.data.children && (
<ul>
{item.data.children
.sort((a: Note, b: Note) => a.data.title.localeCompare(b.data.title))
.map((child: Note) => (
<li
class="before:content-['└'] before:pe-[0.3rem] mt-[0.1rem]"
style={`padding-left: ${level === 0 ? 0 : 1.4}rem`}
>
<a href={`/${child.slug}/`} class="link">
{getLinkText(child)}
</a>
{/* Recursively render grandchildren, etc */}
{getChildren(child, level + 1)}
</li>
))}
</ul>
)
return (
<li class="mb-2 break-inside-avoid-column">
<a href={`/${note.slug}/`} class="link">
{getLinkText(note)}
</a>
{getChildren(note, 0)}
</li>
)
})
}
</ul>
Maintaining note hierarchies is a chore
That was a fun programming puzzle to solve, but unfortunately it made my note-taking more difficult by forcing me to decide where each new thought belonged in the hierarchy before I could jot it down.
To remove that friction, I’ve adopted to a flat (and eventually searchable and filterable) approach to note-taking instead. But I wanted to share this former nested-topics solution in case anyone else finds it useful.