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