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(内联元素),会导致内部所有的 marginpadding 计算彻底崩溃。这就解释了为什么各种排版和偏移量会如此诡异。

定位到病因后,解法就非常清晰了:破旧立新。

在解密完成的回调函数中,执行以下三步:

  1. 剥离流氓属性: tocContainer.removeAttribute('style') 清除 display: inline,恢复正常的块级盒模型。
  2. 注入专属 CSS: 利用原生 CSS 的 counter 属性重新补齐被遗弃的序号(1.1, 1.2)和缩进引导线。
  3. 唤醒沉睡的 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>

大功告成!