Photo - Leiada Krözjhen
Photo

Folder Notes in Obsidian Publish

Recently, I've been participating in some very fun TTRPG games. Obsidian has become my favorite tool for taking notes. One plugin I've found which I quite like is Folder Notes.

I ended up getting a Publish subscription, and wanted mimic the behavior of Folder Notes on the published site. This weekend, I sat down and hacked on it a bit. I'm relatively pleased with the end result and wanted to share.

I'm happy to report Obsidian feels pleasantly hackable, it's clearly a tool designed with malleability in mind. I'm not much of a JS hacker anymore, but this was fun enough that I look foward to doing more. Besides, it's nice to flex old muscles occasionally.

Implementation

I found this implementation by gardener which was a good start, but didn't quite work how I wanted it to. So I took the ideas from it and wrote my own.

Without further ado, add the following to your publish.js:

// publish.js

/** Selects an expandable. */
const folderNoteExpandableSelector = "div.tree-item-self.mod-collapsible:not(.mod-root)";
/** Selects an individual menu item. */
const foderNoteIndividualSelector = "div.tree-item-self:not(.mod-collapsible):not(.mod-root)";

/**
 * If the provided `expandableElement` has a Folder Note, wire up navigation.
 * @param {HTMLElement} expandableElem 
 * @returns 
 */
function updateFolderNoteExpandable(expandableElem) {
    let path = expandableElem.getAttribute("data-path");

    // Folder notes by default have the format `{parents}/{folder}/{folder}.md`
    // If you configured this differently, you might need to update this.
    let pathWithFolderNote = `${path}/${path.split("/").last()}.md`;

    // Obsidian's `app.site.cache.getCache` lets us find if the page exists.
    // We can't just scan the DOM, since the element might not exist yet.
    let detectedFolderNote = app.site.cache.getCache(pathWithFolderNote);
    if (detectedFolderNote === null || detectedFolderNote === undefined) {
        return;
    }
    expandableElem.classList.add("has-folder-note");
    
    // Add our own click handler to the expandable that uses Obsidian's `app.navigate`,
    // doing an `<a href="...">` results in page reload jank.
    let expandableInnerElem = expandableElem.querySelector("div.tree-item-inner");
    expandableInnerElem.addEventListener("click", _ => {
        app.navigate(pathWithFolderNote, "", null);
    });
 
    // We stash the children to use later on the replacement, they keep their events.
    let expandableChildren = expandableElem.childNodes;
    
    // Strip click events off the expandable by cloning it.
    let replacerElem = expandableElem.cloneNode(false);
    // Put the children back (with events)
    replacerElem.replaceChildren(...expandableChildren);
    // Swap the old evented element with the new one we've wired in.
    expandableElem.replaceWith(replacerElem);
}

/**
 * If the provided `individualElem` is a Folder Note, hide it.
 * @param {HTMLElement} individualElem 
 * @returns 
 */
function updateFolderNoteIndividual(individualElem) {
    let individualElemPath = individualElem.getAttribute("data-path");
    
    // If a folder note exists, it'll be called `{parents}/{note_without_extension}`
    let fileName = individualElemPath.split("/").last();

    if (!fileName.endsWith(".md")) {
        // This is a folder... That's not good. Bail.
        console.warn("Got an individual update for a folder?", individualElem);
        return;
    }

    let fileNameExtensionless = fileName.replace(".md", "");
    let folderNotePath = individualElemPath.replace(`${fileNameExtensionless}/${fileName}`, fileNameExtensionless);
    if (individualElemPath == folderNotePath) {
        // This item did not have the makings of a Folder Note, the path didn't make sense.
        return;
    }

    // See if a folder exists that this is a Folder Note for.
    let folderNoteSelector = `${folderNoteExpandableSelector}[data-path="${folderNotePath}"]`;
    let detectedFolderNoteElem = document.querySelector(folderNoteSelector);
    if (detectedFolderNoteElem === null) {
        // This is not the Folder Note for a menu item.
        return;
    }

    individualElem.classList.add("folder-note");
}

/**
 * Wire up Folder Notes for anything in the `rootElem`.
 * @param {HTMLElement} rootElem 
 */
function updateFolderNotes(rootElem) {
    for (individualElem of rootElem.querySelectorAll(foderNoteIndividualSelector)) {
        updateFolderNoteIndividual(individualElem);
    }
    for (expandableElem of rootElem.querySelectorAll(folderNoteExpandableSelector)) {
        updateFolderNoteExpandable(expandableElem);
    }
}

/**
 * Start up the `MutationObserver` for Folder Notes.
 */
function injectFolderNoteObserver() {
    let config = { childList: true, subtree: true };
    let callback = (mutationList, _observer) => {
        for (let mutation of mutationList) {
            if (mutation.type !== "childList") {
                return;
            }
            for (addedNode of mutation.addedNodes) {
                updateFolderNotes(addedNode);
            }
        }
    };
    updateFolderNotes(document);
    let observer = new MutationObserver(callback);
    let nav = document.querySelector("div.nav-view");
    observer.observe(nav, config);
}

injectFolderNoteObserver();

Then, in your publish.css add this rule:

/* publish.css */
div.folder-note {
	display: none;
}

Caveats

You apparently need a Custom Domain for this. I had one, so it wasn't a problem for me.

This may break in future versions of Obsidian Publish. I likely won't update this unless I am still using Obsidian Publish myself, and Folder Notes, and notice, and remember to update this page. You might need to hack yourself, so I left it well commented.

            32da56788cdd43f9f265886594502f04e9dcb923