笔记 - 网站更新记录

TOC

更新网站搜索

网站菜单栏的搜索原本是全站所有页面都搜索,搜索内容包括:标题,内容。

要更新的原因:

  • 我注意到index.json这个文件比较大,这才没几篇文章呢,这个文件就有90kb了;
  • 索引包括了如 home, tags 这些页面,这部分没有意义,需要去掉;

计划更新后的全站搜索内容包括:标题,summary。基于此,需要额外编辑内容的summary,因为直接取前summaryLength定义的数量不准确。

注意

summaryLength 和之前的不太一样,以前是字符数量,现在变成了段落数量(下方的说明还是说的是字词数量,但是我实际使用的时候发现用的确实是段落数量,可能是一个bug,也可能是我自己使用不对 —— 这不太可能,毕竟我就是正常在用,我的hugo版本是: hugo v0.145.0+extended+withdeploy darwin/arm64 BuildDate=2025-02-26T15:41:25Z VendorInfo=brew

(int) Applicable to automatic summaries, the minimum number of words returned by the Summary method on a Page object. The Summary method will return content truncated at the paragraph boundary closest to the specified summaryLength, but at least this minimum number of words.

建立全站搜索 - 本次更新前

在 /themes/blog/layouts/_default/index.json:

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.Pages -}}
  {{- $.Scratch.Add "index" (dict
    "title" .Title
    "content" .Plain
    "url" .Permalink
  ) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

在 main.js 中搜索相关的内容:

// 初始化搜索功能
function initSearch() {
  const searchToggle = document.getElementById('search-toggle');
  const searchBox = document.getElementById('search-box');
  const searchInput = document.getElementById('search-input');
  const searchResults = document.getElementById('search-results');
  const clearSearchButton = document.getElementById('clear-search');

  if (searchToggle && searchBox && searchInput && searchResults && clearSearchButton) {
    // 显示/隐藏搜索框
    searchToggle.addEventListener('click', () => {
      searchBox.style.display = searchBox.style.display === 'block' ? 'none' : 'block';
      searchInput.focus();
    });

    // 点击页面其他区域隐藏搜索框
    document.addEventListener('click', (event) => {
      const isClickInsideSearchBox = searchBox.contains(event.target); // 判断点击目标是否在搜索框内部
      const isClickOnSearchToggle = searchToggle.contains(event.target); // 判断点击目标是否是搜索图标

      // 如果点击的目标不在搜索框内部,也不是搜索图标,则隐藏搜索框
      if (!isClickInsideSearchBox && !isClickOnSearchToggle) {
        searchBox.style.display = 'none';
      }
    });

    // 加载搜索索引
    let searchIndex = [];
    fetch('/index.json')
      .then(response => response.json())
      .then(data => {
        searchIndex = data;
      });

    // 搜索功能
    searchInput.addEventListener('input', () => {
      const query = searchInput.value.toLowerCase();

      // 筛选结果
      const results = searchIndex.filter(item => {
        return (
          item.title.toLowerCase().includes(query) ||
          item.content.toLowerCase().includes(query)
        );
      });

      // 去重:基于 URL 去重
      const uniqueResults = [];
      const seenUrls = new Set(); // 用于记录已经处理过的 URL

      for (const item of results) {
        if (!seenUrls.has(item.url)) {
          seenUrls.add(item.url); // 将 URL 添加到 Set 中
          uniqueResults.push(item); // 将去重后的结果添加到数组中
        }
      }

      // 按匹配度排序(标题匹配的优先级高于内容匹配)
      uniqueResults.sort((a, b) => {
        const aTitleMatch = a.title.toLowerCase().includes(query);
        const bTitleMatch = b.title.toLowerCase().includes(query);

        if (aTitleMatch && !bTitleMatch) return -1; // a 优先
        if (!aTitleMatch && bTitleMatch) return 1; // b 优先
        return 0; // 保持顺序
      });

      // 显示搜索结果
      searchResults.innerHTML = uniqueResults
        .map(item => `<li><a href="${item.url}">${item.title}</a></li>`)
        .join('');
    });

    // 清除搜索内容
    clearSearchButton.addEventListener('click', () => {
      searchInput.value = ''; // 清空输入框内容
      searchResults.innerHTML = ''; // 清空搜索结果
      searchInput.focus(); // 聚焦到输入框
    });
  }
}

建立全站搜索 - 本次更新后 - 20250326

首先,确定需要索引的section, 为了便于后期维护,我在hugo.yaml定义了这部分:

params:
  searchInSection:
    - posts
    - books
    - english

然后编辑 /themes/blog/layouts/_default/index.json:

{{- $sectionsToIndex := .Site.Params.searchInSection | default (slice "posts") -}}
{{- $.Scratch.Add "index" slice -}}
{{- range where .Site.Pages "Section" "in" $sectionsToIndex -}}
  {{- $.Scratch.Add "index" (dict
    "title" .Title
    "summary" (.Summary | plainify)
    "url" .Permalink
  ) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
  • 如果没有配置,默认只索引 “posts” section;
  • 索引内容移除了页面的内容,增加了页面的摘要;
  • 摘要净化处理: (.Summary | plainify) ,移除摘要中的 HTML 标签

相关的 main.js:

searchInput.addEventListener('input', () => {
  const query = searchInput.value.toLowerCase();
  const results = searchIndex.filter(item => {
    return (
      item.title.toLowerCase().includes(query) ||
      (item.summary && item.summary.toLowerCase().includes(query))
    );
  });
  
  // 显示结果...
});

至此,全站的搜索就更新完成了,优点是index.json的内容减少了,缺点当然就是搜索的范围缩小了,不过通过认为定义summary 也可以浓缩页面的主旨。

建立section搜索

如果不要全站搜索,只要比如posts/下边的内容,当然可以只保留posts,就可以了。

但是我考虑的是后期要给 books, english 建立单独的搜索,并且同时要保留全站搜索,所以,需要新建。

这里记录一下deepseek给的回复,例子用的是只要posts/的内容。

以下内容应该被删除,因为实践不可行 - 20250329

  • 创建专用的 posts.json 索引文件

    在 layouts/_default/ 目录下创建一个新的模板文件 posts.json,只包含 /posts/ 下的内容。

    {{- $.Scratch.Add "index" slice -}}
    {{- range where .Site.Pages "Section" "posts" -}}
    {{- $.Scratch.Add "index" (dict
        "title" .Title
        "content" .Plain
        "url" .Permalink
    ) -}}
    {{- end -}}
    {{- $.Scratch.Get "index" | jsonify -}}
    

    这里需要注意搜索的范围是:标题,内容。如果需要可以自己改。

  • 修改 JavaScript 代码,加载 /posts.json 而非 /index.json:

    fetch('/posts.json')  // 替换原来的 fetch('/index.json')
    .then(response => response.json())
    .then(data => {
      searchIndex = data;
    });
    

总结:先说下上边的方法为什么不可行,首先创建的posts.json文件压根不会生成,根据deepseek的说明,我反复尝试过修改hugo.yaml,调整posts.json到posts/下等,结果都不可以,所以改版成现在这样:整个网站只有一个index.json文件,在文件索引部分加上一个section,然后各个 Section 的 list 页面可以查询本 Section 相关的内容,这种在我看来其实才是最简单实用的。

这次(2025-03-29)改动后实现的效果是上边的总结所说的,具体的实现方法和需要注意的地方在这里记录一下:

index.json需要手动调整

{{- $index := slice -}}
{{- $sections := .Site.Params.searchInSection | default (slice "posts") -}}
{{- range .Site.RegularPages -}}
  {{- if in $sections .Section -}}
    {{- $item := dict "section" .Section "url" .RelPermalink -}}
    
    {{/* 调试信息 */}}
    {{/*
    {{- warnf "Processing page: %s (Section: %s)" .File.Path .Section -}}
    */}}

    {{/* 公共字段 */}}
    {{- $item = merge $item (dict "title" (.Title | default "Untitled")) -}}
    
    {{/* Section特定字段 */}}
    {{- if eq .Section "posts" -}}
      {{- $item = merge $item (dict "summary" (.Summary | plainify | default "")) -}}
    {{- else if eq .Section "books" -}}
      {{- $item = merge $item (dict 
        "author" (.Params.author | default "Unknown")
        "isbn" (.Params.isbn | default "")
      ) -}}
    {{- end -}}
    
    {{- $index = $index | append $item -}}
  {{- end -}}
{{- end -}}

{{/*
{{- warnf "Generated %d search items" (len $index) -}}
*/}}

{{- $index | jsonify -}}

各个section因为各自内容设计的不同,所以需要索引的也不一样。

main.js 需要手动调整

const results = searchIndex.filter(item => {
      // 根据当前 section 决定搜索哪些字段
      if (currentSection === 'posts') {
        return (
          item.title.toLowerCase().includes(query) ||
          (item.summary && item.summary.toLowerCase().includes(query))
        );
      } else if (currentSection === 'books') {
        return (
          item.title.toLowerCase().includes(query) ||
          (item.author && item.author.toLowerCase().includes(query)) ||
          (item.isbn && item.isbn.toLowerCase().includes(query))
        );
      }
      return false;
    });

    // 按匹配度排序
    results.sort((a, b) => {
      // 标题匹配优先
      const aTitleMatch = a.title.toLowerCase().includes(query);
      const bTitleMatch = b.title.toLowerCase().includes(query);
      if (aTitleMatch && !bTitleMatch) return -1;
      if (!aTitleMatch && bTitleMatch) return 1;

      // 其次摘要/作者匹配
      const aSecondaryMatch = currentSection === 'posts'
        ? a.summary?.toLowerCase().includes(query)
        : a.author?.toLowerCase().includes(query);
      const bSecondaryMatch = currentSection === 'posts'
        ? b.summary?.toLowerCase().includes(query)
        : b.author?.toLowerCase().includes(query);

      if (aSecondaryMatch && !bSecondaryMatch) return -1;
      if (!aSecondaryMatch && bSecondaryMatch) return 1;

      return 0;
    });

可以看到这里的搜索是写明了postsbooks相关的内容,后期如果要修改或者新增,也需要在这里再次编辑。

另外增加了一个搜索结果匹配高亮的效果。

新增/books/ 20250330

新增了一个section: books. 用来记录看过的书以及对应的读后感和总结。

前后还是折腾了一下,最后才改成现在这种最简单的形式,这里记录一下整个过程。

最开始计划的是要能实现直接阅读,保存阅读进度,下载书籍,链接读后感,书籍分类,书籍信息…

现在:显示书籍基本信息,读后感。

我觉得,网站还是得轻巧才行,如果要实现直接阅读,那么加载一本书(或者实现分页),还要考虑到书的格式(EPUB 还需要调用外部库),想想都庞杂,所以,结合我自己的实际情况,我就用苹果自带的图书app看书,看完我就记录自己的想法就可以了,书籍基本信息里有一个isbn,需要的书的话自己查,而且最不济的情况下:大不了我后期把这些书都上传到github上去,需要的时候直接去下载,倒也放心。

在这个过程中查到几个开源的,可以在线使用的电子书阅读应用: github topic ebook

添加页面隐藏 20250331

新增了一个.Params.hide, 用来实现页面隐藏。有些时候不想删除一篇文章,但是出于一些原因不想显示在网站上,就在front matter里加上这个。

目前已经实现了在 home, list, index.json 都可以隐藏,没有实现的是 /tags/页面的tag count, 也就是说如果一个页面被隐藏了,但是有tag,那么在统计这个tag的数量的时候还是会把这篇文章给算上,不过你点进去找不到这个文章,所以会造成一个现象:比如你看 tools 这个tag显示有5篇文章,但是你点进去会发现只有4个链接,原因可能就是其中一篇文章被隐藏了,但是它的tags包含了tools 这个词。

我倒想直接改进实现统一,但是deepseek给的解决方法我认为比较麻烦,所以先搁置,以后再说。

总结: 在这个过程中,我还改了网站的图片存储位置,都放到了static文件夹下,这就导致了一个问题:以前放在其他地方的,比如图片,现在他们的路径都变了,那么使用这些图片的文件又有哪些,具体又在这些文件的什么位置,我是不是还得一个个去找到然后修改了,要是我足够细心加耐心,把他们一个个手动改正并且改对了,那还好,那如果我接下来又换了一次这些图片的路径,或者改了一个图片的名称,那我…

想想都庆幸自己一开始的时候就把这些变量放到hugo.yaml这个配置文件中,使用的时候都是来引用这个文件的内容,所以如果要改动什么文件的位置或者名称,在这里改动一次,其他引用的地方自己就更新了。

想想这个过程,逻辑,升华一下…