前言 链接到标题

注释
  • 笔者苦没有侧栏索引久已,没有这个书签一样的跳转功能,对于一些长笔记博客就是噩梦,一不小心刷新一下就要从头开始往下找,并且markdown独特的head功能也损失殆尽。
  • 寻找建立侧栏目录的过程几乎在创好博客之后不就久开始了,然而找到的其他主题的,比如stake、papermod等都有原生的侧栏目录,或者说是通用hugo的侧栏对于coder都不适用(当然,也可能是我再操作过程中没有完全复刻指引操作),尝试ai量身定做,或者是在coder仓库里用ai发挥,但是没有一种方案是能实现我想要的侧栏效果的。
  • 看来这个效果还是有一定复杂性的,对于ai来说有点难理解,那么解决问题最好办法还是复现,
  • 于是我尝试发issues,但是可能是我描述不清,我开的issues两周都没有人回复。
  • 在这两周之内时不时我还是尝试着前面提到的各种方法,包括网页搜索、ai,但是均无成效。
  • 今天突发奇想,我似乎之前用github copilot只是在读他的仓库代码,还没有用来整理过已有的77条issues,或许其中有我想要的答案?
  • 果然,在第一轮检索的结果中ai就找到了我满意的结果,这个需求是有人已经实现过的,但是作者倾向于顶部目录,因此这个issue的回答者最终没有pull上来,因此没有构成coder的原生功能。
  • 在AI的帮助下,我把那位贡献者的解决方案整理成了操作步骤,并成功实现了想要的效果。下面是我的详细记录(我之前完全没学过前端,并不指望能理解全部代码,只要能跟着教程实现功能并做些简单调整就满足了)。
  • 如果你也想要这个效果,或者说你也在寻找coder主题的侧栏目录的实现方法,那么可以参考下面的步骤。
这个优化功能源于ckvv

初步修改:复现 链接到标题

第一步要做的就是看到这个效果,至于之后怎么调,都要在“能实现”的基础上进行。

保存ckvv的代码 链接到标题

将ckvv提供的代码:

function debounce(func, wait, options = {
  immediate: false,
  middle: true,
  thisArg: null,
}) {
  let timer;
  let restDate = new Date();
  const immediate = options.immediate !== false;
  const middle = options.middle !== false;
  const thisArg = options.thisArg || null;
  return function (...args) {
    timer && clearTimeout(timer);
    let isFirst = !timer;
    timer = setTimeout(() => {
      func.apply(thisArg, args);
      restDate = new Date();
    }, wait);
    if ((new Date() - restDate > wait && middle) || (isFirst && immediate)) {
      clearTimeout(timer);
      func.apply(thisArg, args);
      restDate = new Date();
    }
  }
}

function setActive(anchors) {
  const ele = anchors.find((ele, index, arr) => {
    return ele.getBoundingClientRect().top >= 0 || index >= arr.length - 1;
  });
  if (ele) {
    const tableOfContents = document.querySelector('#table-of-contents');
    const toActive = tableOfContents.querySelector(`a[href="#${ele.id}"]`);
    if (!toActive) return;
    const activeA = tableOfContents.querySelector(`.active`);
    if (activeA) activeA.classList.remove('active');
    toActive.classList.add('active');
    window.history.pushState(null, null, `#${ele.id}`);
    tableOfContents.scrollTo({
      left: 0,
      top: toActive.offsetTop - tableOfContents.getBoundingClientRect().height / 2,
      behavior: 'smooth',
    });
  }
}

function initContents(icon = '<svg t="1690868184633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3381" width="32" height="32"><path d="M128 192l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3382"></path><path d="M128 448l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3383"></path><path d="M128 704l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3384"></path></svg>') {
  if(document.querySelector('#table-of-contents-wapper')) return;
  const contents = document.createElement('details');
  contents.id = 'table-of-contents-wapper';
  contents.innerHTML = `<summary>
    ${icon}
  </summary>`
  const styleElement = document.createElement('style')
  styleElement.innerHTML = `#table-of-contents-wapper {
    user-select: none;
    position: fixed;
    right: 1em;
    top: 4em;
    border-radius: 8px;
    z-index: 999
  }

  @media only screen and (min-width:768px) {
    #table-of-contents-wapper[open] summary {
      position: relative;
      left: 5.8em
    }
  }

  @media only screen and (max-width:768px) {
    #table-of-contents-wapper {
      top: auto;
      right: auto;
      bottom: 1.8rem;
      left: 1.8rem
    }
  }

  #table-of-contents-wapper summary {
    display: inline-block;
    font-size: 1.5em;
    border-radius: 4px;
    cursor: pointer;
    padding: .2em
  }

  #table-of-contents-wapper #table-of-contents {
    line-height: 1.3;
    width: 19rem;
    font-size: .8rem;
    padding: .8em;
    border: 1px solid;
    border-radius: 6px;
    overflow-y: scroll;
    max-height: calc(100vh - 20rem);
    color: currentColor
  }

  #table-of-contents-wapper #table-of-contents a {
    width: 100%;
    display: inline-block;
    color: currentColor;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis
  }

  #table-of-contents-wapper #table-of-contents .active {
    border-left: 2px solid #42a5f5;
    color: #42a5f5
  }

  .content h1,
  .content h2,
  .content h3 {
    padding-top: 2em !important;
    margin-top: -2em !important
  }`;

  const anchors = [...document.querySelector('.content').querySelectorAll('h1[id],h2[id],h3[id],h4[id]')];
  if (!anchors.length) return contents.remove();
  const tableOfContents = document.createElement('div');
  tableOfContents.id = 'table-of-contents';
  anchors.forEach(ele => {
    const a = document.createElement('a');
    if (!ele.innerText) return;
    a.innerText = ele.innerText;
    a.href = `#${ele.id}`;
    a.style.paddingLeft = `${ele.tagName.charAt(1)}em`;
    tableOfContents.appendChild(a);
  });

  contents.appendChild(tableOfContents);
  contents.open = window.innerWidth >= 768;

  document.head.appendChild(styleElement);
  document.body.append(contents);

  setActive(anchors);
  const debounceSetActive = debounce(setActive, 200)
  window.addEventListener('scroll', () => {
    debounceSetActive(anchors);
  });
}

保存在/static/js/目录下,命名为toc-sidebar.js

修改footer.html 链接到标题

对于/layouts/partials/footer.html文件,添加如下代码:

<!-- 用于增加侧栏 -->
<script src="/js/toc-sidebar.js"></script>
<script>
  window.addEventListener('DOMContentLoaded', function() {
    initContents();
  });
</script>
提示
ai说也可以在/layouts/partials/beseof.html文件中添加,但是我没有尝试。

修改single.html 链接到标题

修改后如下

{{ define "title" }}
  {{ .Title }} · {{ .Site.Title }}
{{ end }}

{{ define "content" }}
  {{ partial "page.html" . }}
  {{ partial "posts/math.html" . }}
{{ end }}

然后可以运行一下hugo server,应该就已经实现了,笔者的视线效果如下:

alt text

优化 链接到标题

调整字号 链接到标题

显然,这个字号是相当小的,于是将js文件输给ai,ai告诉我了如何改,请将下面的代码覆盖进js文件中对应的函数,然后根据注释指引修改

function initContents(icon = '<svg t="1690868184633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3381" width="32" height="32"><path d="M128 192l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3382"></path><path d="M128 448l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3383"></path><path d="M128 704l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3384"></path></svg>') {
  if(document.querySelector('#table-of-contents-wapper')) return;
  const contents = document.createElement('details');
  contents.id = 'table-of-contents-wapper';
  contents.innerHTML = `<summary>
    ${icon}
  </summary>`
  const styleElement = document.createElement('style')
   // ...existing code...
  
    styleElement.innerHTML = `#table-of-contents-wapper {
      user-select: none;
      position: fixed;
      right: 1em;
      top: 4em;
      border-radius: 8px;
      z-index: 999
    }
  
    @media only screen and (min-width:768px) {
      #table-of-contents-wapper[open] summary {
        position: relative;
        left: 5.8em
      }
      
      /* 让文章内容靠左 */
      .content {
        margin-right: 22rem; /* 给右侧预留足够空间 */
        max-width: calc(100% - 25rem);
      }
    }
  
    @media only screen and (max-width:768px) {
      #table-of-contents-wapper {
        top: auto;
        right: auto;
        bottom: 1.8rem;
        left: 1.8rem
      }
    }
  
    #table-of-contents-wapper summary {
      display: inline-block;
      font-size: 1.5em;
      border-radius: 4px;
      cursor: pointer;
      padding: .2em
    }
  
    #table-of-contents-wapper #table-of-contents {
      line-height: 1.5; /* 增加行高改善可读性 */
      width: 22rem; /* 增加宽度,从19rem到22rem */
      font-size: 0.95rem; /* 增大字号,从0.8rem到0.95rem */
      padding: 1em;
      border: 1px solid;
      border-radius: 6px;
      overflow-y: scroll;
      max-height: calc(100vh - 20rem);
      color: currentColor
    }
  
    #table-of-contents-wapper #table-of-contents a {
      width: 100%;
      display: inline-block;
      color: currentColor;
      line-height: 20; /* 增加行高 */
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      padding: 0.15em 0; /* 增加上下内边距,让链接更易点击 */
    }
  
    #table-of-contents-wapper #table-of-contents .active {
      border-left: 2px solid #42a5f5;
      color: #42a5f5
    }
  
    .content h1,
    .content h2,
    .content h3 {
      padding-top: 2em !important;
      margin-top: -2em !important
    }`;
  
  // ...existing code...

  const anchors = [...document.querySelector('.content').querySelectorAll('h1[id],h2[id],h3[id],h4[id]')];
  if (!anchors.length) return contents.remove();
  const tableOfContents = document.createElement('div');
  tableOfContents.id = 'table-of-contents';
  anchors.forEach(ele => {
    const a = document.createElement('a');
    if (!ele.innerText) return;
    a.innerText = ele.innerText;
    a.href = `#${ele.id}`;
    a.style.paddingLeft = `${ele.tagName.charAt(1)}em`;
    tableOfContents.appendChild(a);
  });

  contents.appendChild(tableOfContents);
  contents.open = window.innerWidth >= 768;

  document.head.appendChild(styleElement);
  document.body.append(contents);

  setActive(anchors);
  const debounceSetActive = debounce(setActive, 200)
  window.addEventListener('scroll', () => {
    debounceSetActive(anchors);
  });
}

同时这一部分将文章尽可能的靠左,从而减少了挤在一起的情况,至于为什么呢?问ai。 效果如下: alt text

明显舒服多了。 至此,初代侧栏目录就完成了。感谢ckvv在issues提供的启发帮助!