hexo-blog-encrypt 插件无法加密很多主题的文章目录,我使用的 hexo-theme-yun 也是如此。这篇文章记录了如何解决这个问题。

Demo:

hexo-theme-yun 主题上最新的一个 issue 里面提到的就是这个问题。

对于这个问题的解决,在 hexo-blog-encrypt 插件的 readme 里面已经提到了一些关于加密目录的解决方案,它需要修改主题的源代码,而且对于不同的主题,修改的地方也不一样。

对于 hexo-theme-yun 主题,在我研究了一个下午后,我的基本结论是:

hexo-theme-yun 主题无法使用 hexo-blog-encrypt 插件加密文章目录。

因为我发现 hexo-theme-yun 主题的文章目录是在 sidebar 里面的,而 hexo-blog-encrypt 插件加密的是文章内容,所以这个问题是无法解决的。

好吧,这确实是一个标题党的文章,下面记录一下我是如何优雅解决这个问题的。

问题一:目录无法渲染

在研究代码的过程中,我发现对于设置的密码的文章,hexo-theme-yun 甚至无法正确渲染出文章的目录。

这是因为它使用 page.content 来渲染文章内容,而 hexo-blog-encrypt 插件会加密 page.content,所以导致目录无法正确渲染。

https://github.com/YunYouJun/hexo-theme-yun/blob/89f68d9aa0ddfd25acd3b1001df9ab75e9d770cb/packages/hexo-theme-yun/layout/_partial/sidebar.pug#L14

- var toc_content = toc(page.content, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})

要解决这个问题,我们需要使用 page.origin 来生成文章的目录。

这里我把它改成了首先判断是否加密,如果加密了就使用 page.origin,否则使用 page.content

- var toc_content = page.encrypt ? toc(page.origin, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth}) : toc(page.content, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})

改了这行代码,目录就可以正确渲染了。

问题二:目录无法隐藏

但是,这样做还是有一个问题,就是对于加密了的文章,目录无法正确隐藏。

可以从代码里面看出如果目录里面有内容,就会直接显示目录。

https://github.com/YunYouJun/hexo-theme-yun/blob/89f68d9aa0ddfd25acd3b1001df9ab75e9d770cb/packages/hexo-theme-yun/layout/_partial/sidebar.pug#L78

if display_toc
  #post-toc-wrap.sidebar-panel(class=toc_content.length > 1 ? 'sidebar-panel-active' : '')
    .post-toc
      .post-toc-content
        != toc_content.length > 1 ? toc_content : theme.toc.placeholder || __('post.toc_empty')

因为目录本身不会被加密,为了实现加密的效果,在生成目录时判断文章是否加密,如果是加密了的文章就用 CSS 的方式把目录隐藏起来。

然后再通过 JS 脚本来监听解密事件,解密后再把目录的 CSS 样式改成显示。

if display_toc
  #post-toc-wrap.sidebar-panel(class=toc_content.length > 1 ? 'sidebar-panel-active' : '')
    .post-toc
      .post-toc-content
        if page.encrypt
          #toc-div(style="display:none")
            != toc(page.origin, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})
          script.
            window.addEventListener('hexo-blog-decrypt', function() {
              var tocDiv = document.getElementById('toc-div');
              if (tocDiv) {
                tocDiv.style.display = 'block';
              }
            });
        else
          != toc_content.length > 1 ? toc_content : theme.toc.placeholder || __('post.toc_empty')

这样就可以实现目录的隐藏和显示了。

在 hexo-theme-yun 主题的 sidebar 里面,如果目录有内容,那么目录就是 active 的,如果目录没有内容,那么信息页就是 active 的。

在我的这个加密方法里面,目录内容实际上是存在的,只是被 CSS 隐藏了,所以对于加密了的文章,进入文章页后,目录页是 active 的,但却是空空如也。

所以这里我们需要优化一下 sidebar 的 active 逻辑,改为 如果目录有内容且文章没有加密,那么目录页是 active 的

否则,信息页是 active 的。

  if display_toc
    ul.sidebar-nav
      li.sidebar-nav-item.sidebar-nav-toc.hty-icon-button(data-target='post-toc-wrap', title=__('sidebar.toc'), class=(toc_content && !page.encrypt) ? 'sidebar-nav-active' : '')
      +icon('ri:list-ordered')
      li.sidebar-nav-item.sidebar-nav-overview.hty-icon-button(data-target='site-overview-wrap', title=__('sidebar.overview'), class=(toc_content && !page.encrypt) ? '' : 'sidebar-nav-active')
      +icon('ri:passport-line')

...

  #post-toc-wrap.sidebar-panel(class=(toc_content.length <= 1 || page.encrypt) ? '' : 'sidebar-panel-active')

完整的 patch

完整的修改如下:

diff --git a/themes/hexo-theme-yun/layout/_partial/sidebar.pug b/themes/hexo-theme-yun/layout/_partial/sidebar.pug
index b4214d8..435ebd6 100755
--- a/themes/hexo-theme-yun/layout/_partial/sidebar.pug
+++ b/themes/hexo-theme-yun/layout/_partial/sidebar.pug
@@ -11,15 +11,16 @@ aside.sidebar
   //- can not be async
   //- https://stackoverflow.com/questions/9237044/async-loaded-scripts-with-domcontentloaded-or-load-event-handlers-not-being-call
   script(src=url_for(theme.cdn.pre + '/js/sidebar.js'), type='module')
-  - var toc_content = toc(page.content, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})
+  - var toc_content = page.encrypt ? toc(page.origin, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth}) : toc(page.content, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})
   if display_toc
     ul.sidebar-nav
-      li.sidebar-nav-item.sidebar-nav-toc.hty-icon-button(data-target='post-toc-wrap', title=__('sidebar.toc'), class=toc_content ? 'sidebar-nav-active' : '')
+      li.sidebar-nav-item.sidebar-nav-toc.hty-icon-button(data-target='post-toc-wrap', title=__('sidebar.toc'), class=(toc_content && !page.encrypt) ? 'sidebar-nav-active' : '')
         +icon('ri:list-ordered')
-      li.sidebar-nav-item.sidebar-nav-overview.hty-icon-button(data-target='site-overview-wrap', title=__('sidebar.overview'), class=toc_content ? '' : 'sidebar-nav-active')
+      li.sidebar-nav-item.sidebar-nav-overview.hty-icon-button(data-target='site-overview-wrap', title=__('sidebar.overview'), class=(toc_content && !page.encrypt) ? '' : 'sidebar-nav-active')
         +icon('ri:passport-line')
 
-  #site-overview-wrap.sidebar-panel(class=display_toc && toc_content.length > 1 ? '' : 'sidebar-panel-active')
+  #site-overview-wrap.sidebar-panel(class=(display_toc && toc_content.length > 1 && !page.encrypt) ? '' : 'sidebar-panel-active')
     include ./sidebar/info.pug
 
     nav.site-state
@@ -92,12 +93,28 @@ aside.sidebar
       a#toggle-mode-btn.links-item.hty-icon-button(href='javascript:;' title="Mode" style='color: #f1cb64')
         +icon('ri:contrast-2-line')
 
   if display_toc
-    #post-toc-wrap.sidebar-panel(class=toc_content.length > 1 ? 'sidebar-panel-active' : '')
+    #post-toc-wrap.sidebar-panel(class=(toc_content.length <= 1 || page.encrypt) ? '' : 'sidebar-panel-active')
       .post-toc
         .post-toc-content
-          != toc_content.length > 1 ? toc_content : theme.toc.placeholder || __('post.toc_empty')
-
+          if page.encrypt
+            #toc-div(style="display:none")
+              != toc(page.origin, {list_number: theme.toc.list_number, max_depth: theme.toc.max_depth, min_depth: theme.toc.min_depth})
+            script.
+              window.addEventListener('hexo-blog-decrypt', function() {
+                var tocDiv = document.getElementById('toc-div');
+                if (tocDiv) {
+                  tocDiv.style.display = 'block';
+                }
+              });
+          else
+            != toc_content.length > 1 ? toc_content : theme.toc.placeholder || __('post.toc_empty')
+    
   if theme.sidebar.tagcloud.enable
     .tag-cloud
       - var start_color = theme.colors.tag_start_color || '#333'

其他已知问题

这样修改之后,目录的隐藏和显示已经可以正常工作了,但是还有一个问题,就是在正确输入密码之后,虽然目录可以显示出来,但是无法实现点击目录跳转到对应的位置。

需要手动刷新页面,然后这个目录就能正常工作了。

关于 hexo-blog-encrypt 插件目前我还发现了一个问题,就是解密后的文章图片没有办法点击放大查看,这个问题我还没有解决。

By The Way

这个博客主题我已经使用了 4 年。主题的作者开发了一个基于 Vue 的新博客框架,名为 Valaxy。我原计划在 2025 年逐步迁移到这个新框架,希望 Valaxy 的加密功能能解决 Hexo 中遇到的问题,但尚未进行实际测试。

经过这个周末的研究,我修复了 Hexo 加密插件与 Dplayer 以及博客目录之间的冲突,因此迁移的紧迫性也不再那么强烈了。

让它再开发一会儿吧。