My OPUS — Obsidian Publish Useful Scripts

Here is my website in Obsidian Publish, https://prabhupada.io, two weeks into discovering Ob.

A huge thanks to everyone who made this possible! An amazing app, fantastic community, rock solid publishing experience, and never did it crash once during upload of 20k files. :slight_smile:

The website has 20,000+ pages, newly converted to Markdown from DokuWiki, and is likely to grow another 15-20k over time. I hope the Obsidian team lets me keep running at this speed, despite Publish being an absolute steal at only $96 per year!

There are self hosted variants like Quartz that offer more options and functionality. Maybe, if it can produce an “offline reader” app, then I’m all in. But running a website for $8/month with no setup and server hassle is just too good. (Again, seriously, thanks!)

Therefore I set out to find scripts and hacks to enhance the experience. I browsed around for a bit, but it seems many people are having similar wishes, with no solution. So I asked AI to help me write some scripts (I’m no coder). So far, these seem the most useful additions:

  • Left sidebar navigation, CSS hack to replace titles
  • Automatic previous and next page navigation buttons
  • Folder notes integrated in navigation (found on forum)

Disclaimer: I’m using the Minimal theme, seeing it has official support, good styling options, and looks like a great starting point. I will clean up my scripts and share here. In time, maybe we can build a decent OPUS Suite, consisting of many such useful scripts.

Top Level Folder Titles

publish.js

/*==================================================
  📁 TOP-LEVEL FOLDER TITLES
==================================================*/
const topLevelFolderSelector = 'div.nav-view > div.tree-item-self.mod-collapsible.mod-root';

function updateTopLevelFolderTitles() {
  document.querySelectorAll(topLevelFolderSelector).forEach(folder => {
    const folderPath = folder.getAttribute('data-path');
    if (!folderPath) return;
    const cache = app.site.cache.getCache(folderPath + '.md');
    if (!cache) return;

    let label = folderPath.split('/').pop();
    if (cache.frontmatter?.title) label = cache.frontmatter.title;
    else if (cache.sections?.length) {
      const h1 = cache.sections.find(s => s.type === 'heading' && s.headingLevel === 1);
      if (h1) label = h1.heading;
    }

    const titleElem = folder.querySelector('.tree-item-inner .tree-item-title');
    if (titleElem) titleElem.textContent = label;

    const noteElem = document.querySelector(`div.tree-item-self:not(.mod-collapsible)[data-path="${folderPath}.md"]`);
    if (noteElem) noteElem.style.display = 'none';
  });
}
new MutationObserver(() => setTimeout(updateTopLevelFolderTitles, 200))
  .observe(document.body, { childList: true, subtree: true });
setTimeout(updateTopLevelFolderTitles, 500);


// Map data-path -> replacement title
const treeTitles = {
  "bg": "Bhagavad-gītā As It Is",
  "sb": "Śrīmad-Bhāgavatam",
  "cc": "Śrī Caitanya-caritāmṛta"
};

document.querySelectorAll('.nav-view .tree-item-self[data-path]').forEach(item => {
  const path = item.getAttribute('data-path');
  const inner = item.querySelector('.tree-item-inner');
  if (!inner) return;

  if (treeTitles[path]) {
    inner.setAttribute('data-title', treeTitles[path]);
    inner.classList.add('has-replacement'); // mark replacement
  } else {
    inner.removeAttribute('data-title');
    inner.classList.remove('has-replacement'); // ensure old names stay visible
  }
});

publish.css

/*==================================================
  🗂 Folder Notes & Tree Items
==================================================*/

/* Hide folder notes from sidebar and file lists */
div.folder-note,
.tree-item-self.folder-note {
    display: none !important;
}

/* Only hide and replace text if a replacement exists */
.nav-view .tree-item-self[data-path] > .tree-item-inner.has-replacement {
    position: relative !important;
    color: transparent !important;
}

.nav-view .tree-item-self[data-path] > .tree-item-inner.has-replacement a {
    color: transparent !important;
    text-decoration: none !important;
}

.nav-view .tree-item-self[data-path] > .tree-item-inner.has-replacement::after {
    content: attr(data-title);
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    font-weight: 600;
    font-size: 13px;
    line-height: 1;
    white-space: nowrap;
    color: var(--text-normal, #111) !important;
}

Automatic Prev & Next button navigation

(function() {
    // Force left sidebar/tree to render
    function ensureSidebar() {
        const toggle = document.querySelector('.workspace-leaf-toggle'); // sidebar toggle button
        if (!toggle) return;

        const sidebar = document.querySelector('.workspace-split.mod-left');
        if (!sidebar || sidebar.style.display === 'none') {
            toggle.click(); // open the sidebar
            setTimeout(() => {
                // optionally close again
                toggle.click();
            }, 500);
        }
    }

    ensureSidebar();

    // Create a button
    function createButton(text, rightOffset) {
        const btn = document.createElement('button');
        btn.textContent = text;
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: '15px',
            right: rightOffset,
            zIndex: '9999',
            padding: '10px 14px',
            backgroundColor: 'indianred',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: 'pointer',
            boxShadow: '0 3px 6px rgba(0,0,0,0.3)',
            pointerEvents: 'auto',
            touchAction: 'manipulation',
            transition: 'background-color 0.2s'
        });

        btn.addEventListener('mouseenter', () => { if (!btn.disabled) btn.style.backgroundColor = '#b22222'; });
        btn.addEventListener('mouseleave', () => { if (!btn.disabled) btn.style.backgroundColor = 'indianred'; });
        btn.addEventListener('touchstart', () => { if (!btn.disabled) btn.style.backgroundColor = '#b22222'; });
        btn.addEventListener('touchend', () => { if (!btn.disabled) btn.style.backgroundColor = 'indianred'; });

        document.body.appendChild(btn);
        return btn;
    }

    const prevBtn = createButton('← Prev', '110px');
    const nextBtn = createButton('Next →', '15px');

    // Find active tree item
    function getActiveItem() {
        return document.querySelector('.tree-item-self.mod-active')?.closest('.tree-item') || null;
    }

    // Update buttons greyed out / enabled
    function updateButtons() {
        const parentTreeItem = getActiveItem();

        if (!parentTreeItem) {
            // If tree not detected, enable both buttons as fallback
            prevBtn.disabled = false;
            nextBtn.disabled = false;
            prevBtn.style.backgroundColor = 'indianred';
            nextBtn.style.backgroundColor = 'indianred';
            prevBtn.style.cursor = 'pointer';
            nextBtn.style.cursor = 'pointer';
            return;
        }

        prevBtn.disabled = !parentTreeItem.previousElementSibling;
        nextBtn.disabled = !parentTreeItem.nextElementSibling;

        prevBtn.style.backgroundColor = prevBtn.disabled ? '#ccc' : 'indianred';
        nextBtn.style.backgroundColor = nextBtn.disabled ? '#ccc' : 'indianred';
        prevBtn.style.cursor = prevBtn.disabled ? 'default' : 'pointer';
        nextBtn.style.cursor = nextBtn.disabled ? 'default' : 'pointer';
    }

    // Navigate to next/prev
    function navigateToSibling(direction) {
        const parentTreeItem = getActiveItem();
        if (!parentTreeItem) return;

        const sibling = direction === 'next'
            ? parentTreeItem.nextElementSibling
            : parentTreeItem.previousElementSibling;

        if (!sibling) return;

        const link = sibling.querySelector('a');
        if (!link) return;

        link.click(); // internal navigation
    }

    nextBtn.addEventListener('click', () => navigateToSibling('next'));
    prevBtn.addEventListener('click', () => navigateToSibling('prev'));

    // Initial update
    updateButtons();

    // Observe DOM changes to keep buttons in sync
    const observer = new MutationObserver(updateButtons);
    observer.observe(document.body, { childList: true, subtree: true });
})();

This code is beta, and subject to change… It works on desktop, but on mobile you have to open the navigation tree first, so we are trying to expose the tree navigation once on load…