记一次 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| 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'); tocContainer.innerHTML = '';
if (!document.getElementById('decrypt-toc-style')) { var style = document.createElement('style'); style.id = 'decrypt-toc-style'; style.innerHTML = ` /* 🎛️ 参数调整区 */ #card-toc .toc-content { --toc-top-gap: 8px; --toc-left-gap: 8px; }
#card-toc .toc-content > ol { margin-top: var(--toc-top-gap) !important; padding-left: var(--toc-left-gap) !important; } #card-toc .toc-content ol { list-style: none !important; counter-reset: btf-toc-numbering; } #card-toc .toc-content ol li { list-style: none !important; } #card-toc .toc-content .toc-link::before { content: counters(btf-toc-numbering, ".") "."; counter-increment: btf-toc-numbering; margin-right: 6px; opacity: 0.85; } #card-toc .toc-content ol ol { padding-left: 10px !important; margin-left: 10px !important; border-left: 1px solid rgb(202, 202, 202); }
/* 🏁 终极修复:自动折叠隐藏未激活的子目录 */ #card-toc .toc-content 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); });
|
同时,在配置文件中:
1 2 3 4 5
| inject: head: bottom: - <script src="/js/decrypt-toc.js"></script>
|
大功告成!