upd: 老掉牙的方法,现在过时了。而且本站已经完全重构,不再使用 hexo.
记一次 Hexo 加密插件与 Butterfly 目录冲突的 Debug 历程。
一直以来,我的博客(Hexo + Butterfly 主题)在使用 hexo-blog-encrypt 插件给文章加密后,侧边栏目录 (TOC) 总会出现匪夷所思的渲染问题:输入密码解密后,目录要么只剩第一级标题,要么层级缩进全无,以及一些严重的像素偏移,看着非常难受。
今天终于有空(其实是重要的事装死看不见)把这个陈年老 bug 修一修,由于不通前端,只能借助 gemini 帮忙。
这里 gemini 的用法是网页端对话(中古 ai 用法),gemini 在这里不能接管我的代码,也不能接管浏览器,思路很容易跑偏,基本上是我在纠偏。
过程
起初,我认为是 Markdown 渲染器的问题,但在不破坏现有依赖环境的前提下,我决定从前端切入。
第一步,理清逻辑:Butterfly 主题默认使用 tocbot.js 生成动态目录。但在加密状态下,页面初次 onload 时文章是隐藏的,tocbot 抓取不到正确的 DOM 树,直接罢工。而解密完成后,主题又不会自动去重新唤醒它。
于是,我尝试通过监听插件自带的前端事件 window.addEventListener('hexo-blog-decrypt', ...),在解密完成后手动重载 tocbot.init()。
目录确实出来了,但排版依然处于崩坏状态(顶部留白极大、缺少原有的 1.1. 序号和左侧引导线)。
经过一系列 CSS 参数微调无效后,我打开了 Chrome DevTools(F12),将正常文章的 DOM 与解密后文章的 DOM 放在一起逐行比对。终于,在 div.toc-content 容器上找到原因。
hexo-blog-encrypt 插件在处理加密 DOM 时,会简单粗暴地给目录外层容器强加一个 style="display: inline;" 以及一个多余的 toc-div-class。
熟悉前端盒模型的都知道,将一个原本应该包裹 <ol> 块级列表的容器强行变成 inline(内联元素),会导致内部所有的 margin 和 padding 计算彻底崩溃。这就解释了为什么各种排版和偏移量会如此诡异。
定位到病因后,解法就非常清晰了:破旧立新。
在解密完成的回调函数中,执行以下三步:
- 剥离流氓属性:
tocContainer.removeAttribute('style')清除display: inline,恢复正常的块级盒模型。 - 注入专属 CSS: 利用原生 CSS 的
counter属性重新补齐被遗弃的序号(1.1, 1.2)和缩进引导线。 - 唤醒沉睡的 tocbot: 按照 Butterfly 主题的标准参数重新初始化
tocbot,并将collapseDepth设为0,完美恢复按需展开的“折叠”特效与平滑滚动。
一行底层 Nodejs 代码没动,仅靠一段游离于系统之外的前端“补丁”脚本,修复了这个屎山级联动 Bug(更屎山了).
代码
在 source/js 下增加文件 decrypt-toc.js :
// 监听 hexo-blog-encrypt 的解密完成事件
window.addEventListener('hexo-blog-decrypt', function () {
console.log('博客已解密,准备重载增强版动态目录...');
setTimeout(function() {
var tocContainer = document.querySelector('#card-toc .toc-content');
if (!tocContainer) return;
// ==========================================
// 🚑 核心修复:剥离加密插件残留的破坏性属性
// ==========================================
tocContainer.removeAttribute('style');
tocContainer.classList.remove('toc-div-class');
// 👇 新增:给解密后的容器打上专属标记,防止 CSS 污染未加密文章
tocContainer.classList.add('decrypted-custom-toc');
// 清空原本的残缺目录
tocContainer.innerHTML = '';
// ==========================================
// ✨ 注入专属 CSS (已增加限定类名隔离)
// ==========================================
if (!document.getElementById('decrypt-toc-style')) {
var style = document.createElement('style');
style.id = 'decrypt-toc-style';
style.innerHTML = `
/* 🎛️ 参数调整区(仅对打上标记的解密目录生效) */
#card-toc .toc-content.decrypted-custom-toc {
--toc-top-gap: 8px;
--toc-left-gap: 8px;
}
#card-toc .toc-content.decrypted-custom-toc > ol {
margin-top: var(--toc-top-gap) !important;
padding-left: var(--toc-left-gap) !important;
}
#card-toc .toc-content.decrypted-custom-toc ol {
list-style: none !important;
counter-reset: btf-toc-numbering;
}
#card-toc .toc-content.decrypted-custom-toc ol li {
list-style: none !important;
}
#card-toc .toc-content.decrypted-custom-toc .toc-link::before {
content: counters(btf-toc-numbering, ".") ".";
counter-increment: btf-toc-numbering;
margin-right: 6px;
opacity: 0.85;
}
#card-toc .toc-content.decrypted-custom-toc ol ol {
padding-left: 10px !important;
margin-left: 10px !important;
border-left: 1px solid rgb(202, 202, 202);
}
/* 🏁 终极修复:自动折叠隐藏未激活的子目录 */
#card-toc .toc-content.decrypted-custom-toc ol.is-collapsed {
display: none !important;
}
`;
document.head.appendChild(style);
}
// ==========================================
// ✨ 初始化目录树
// ==========================================
function initButterflyToc() {
window.tocbot.init({
tocSelector: '#card-toc .toc-content',
contentSelector: '#article-container',
headingSelector: 'h1, h2, h3, h4, h5, h6',
hasInnerContainers: false,
orderedList: true,
listClass: 'toc',
listItemClass: 'toc-item',
linkClass: 'toc-link',
activeLinkClass: 'active',
activeListItemClass: 'active',
collapseDepth: 0,
scrollSmooth: true,
scrollSmoothOffset: 0,
scrollSmoothDuration: 200
});
console.log('✅ 完美版动态目录重载成功!');
}
if (typeof window.tocbot === 'undefined') {
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/tocbot@4.18.2/dist/tocbot.min.js';
script.onload = initButterflyToc;
document.head.appendChild(script);
} else {
initButterflyToc();
}
}, 300);
});
同时,在配置文件中:
inject:
head:
# 不要动原来的
bottom:
- <script src="/js/decrypt-toc.js"></script>
大功告成!