Featured image of post 给HUGO添加文章搜索功能

给HUGO添加文章搜索功能

之前用 Hugo-theme-stack 主题,自带了搜索功能,我曾以为这种功能是 Hugo 默认就有,后来才知道,是外挂。

在更换 Bear cub 主题后,之前也没觉得这功能有多重要,但实际使用中发现,有时候记不起来一些事情,还得借助搜索才行。于是研究了下怎么外挂搜索。其实也很简单,只需四步。

content 目录下新建 search.md 页面

如果不需要放置在导航栏,内容填入默认信息即可,需要进导航栏的,效仿其他 md 文件设置。

1
2
3
4
5
6
7
---
title: "搜索"
date: 2025-11-20T00:00:00+08:00
type: "search"
layout: "search"
---
在此搜索本博客文章。

在 hugo 配置文件中添加 json 输出

比如我的hugo配置文件是 toml 格式,内容如下。

1
2
3
4
5
6
[outputs]
  home = ["HTML", "RSS", "JSON"]  # 添加 JSON 输出

[outputFormats.JSON]
  baseName = "index"
  mediaType = "application/json"

layouts 目录下新建 /search/single.html 模板

模板内容可以参照其他模板修改,搜索功能可定制精确搜索或模糊搜索,大致内容如下。

  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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
{{ define "main" }}
<content>
  <h1>{{ .Title }}</h1>
  <div class="search-box">
    <input
      id="search-input"
      class="search-input"
      type="text"
      placeholder="输入关键词搜索…"
      autocomplete="off"
    />
  </div>

  <ul id="results" class="search-results">
    <li style="color:#666">请输入关键词搜索</li>
  </ul>

</content>

<script>
// ========== 工具函数 ==========
function escapeHtml(str) {
  if (!str) return "";
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

// ========== 精确搜索 ==========
function exactMatch(haystack, needle) {
  if (!haystack || !needle) return false;
  return haystack.toLowerCase().includes(needle.toLowerCase());
}

// ========== 渲染结果 ==========
function renderResults(list) {
  const resultsEl = document.getElementById("results");

  if (!list || list.length === 0) {
    resultsEl.innerHTML = '<li style="color:#666">未找到结果。</li>';
    return;
  }

  const itemsHtml = list.map(item => {
    const title = escapeHtml(item.title || "(无标题)");

    // 外链或本地链接
    const url = escapeHtml(item.link || item.url || "#");
    const isExternal = !!item.link;

    const linkAttrs = isExternal
      ? ' target="_blank" rel="noopener noreferrer"'
      : "";

    // 摘要:取 summary 或 content 开头一段
    const summaryRaw =
      item.summary ||
      (item.content ? item.content.slice(0, 200) + "…" : "");

    const summary = escapeHtml(summaryRaw);

    return `
      <li>
        <div class="sr-title-col">
          <a class="sr-title" href="${url}"${linkAttrs}>${title}</a>
        </div>

        <div class="sr-snippet-col">
          <div class="sr-snippet">${summary}</div>
        </div>
      </li>
    `;
  }).join("");

  resultsEl.innerHTML = itemsHtml;
}

// ========== 加载 index.json 数据 ==========
async function loadIndex() {
  try {
    const res = await fetch("/index.json");
    return await res.json();
  } catch (err) {
    console.error("加载 index.json 失败:", err);
    return [];
  }
}

// ========== 主逻辑 ==========
(async function () {
  const data = await loadIndex();
  const input = document.getElementById("search-input");

  input.addEventListener("input", () => {
    const q = input.value.trim();

    if (!q) {
      renderResults([]);
      return;
    }

    // 精确搜索:title / content / tags 均可匹配
    const result = data.filter(item =>
      exactMatch(item.title, q) ||
      exactMatch(item.content, q) ||
      (item.tags || []).some(t => exactMatch(t, q))
    );

    renderResults(result);
  });
})();
</script>
{{ end }}

layouts 中新建 index.json 模板

主要就是将整个博客文档内容输出到 index.json 文件,搜索时直接在该文件内搜索,可依照自己需求进行修改,比如只搜索标题、摘要、标签等信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[
{{- $pages := where .Site.RegularPages "Type" "not in" (slice "page" "something-you-want-to-exclude") -}}
{{- $first := true -}}
{{- range $i, $p := $pages -}}
  {{- if not $first }},{{ end -}}
  {
    "title": {{ $p.Title | jsonify }},
    "url": {{ $p.RelPermalink | absURL | jsonify }},
    "date": {{ $p.Date.Format "2006-01-02" | jsonify }},
    "summary": {{ with $p.Params.description }}{{ . | jsonify }}{{ else }}{{ $p.Summary | plainify | jsonify }}{{ end }},
    "content": {{ $p.Plain | chomp | jsonify }},
    "tags": {{ $p.Params.tags | jsonify }},
    "categories": {{ $p.Params.categories | jsonify }}
  }
  {{- $first = false -}}
{{- end -}}
]

其他 CSS 配置

如果需要自定义搜索页面的 CSS,可以直接在主题 CSS 或自定义 custom.css 文件中添加,或者写在前边 /search/single.html 模板中也可以。

  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
101
102
103
/* ====== 搜索框区域布局 ====== */
.search-box {
  max-width: 720px;
  margin: 24px 0 32px;
}

.search-input {
  width: 100%;
  padding: 10px 14px;
  font-size: 16px;

  border: 1px solid var(--border);
  border-radius: 8px;

  background: var(--entry);
  color: var(--primary);

  outline: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}

.search-box {
  max-width: 720px;
  margin: 24px 0 32px;
}

.search-input {
  width: 100%;
  padding: 10px 14px;
  font-size: 16px;

  border: 1px solid rgba(150, 150, 150, 0.35);
  border-radius: 8px;

  background: var(--entry);
  color: var(--primary);
  outline: none;

  transition: border-color .15s ease, box-shadow .15s ease;
}

.search-input:focus {
  border-color: var(--text-highlight);
  box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.20);
}

.search-results {
  list-style: none;
  padding: 0;
  margin: 0;
}

.search-results li {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid rgba(0,0,0,0.04);
}

.search-results .sr-title-col {
  flex: 0 0 40%;
  min-width: 180px;
  max-width: 420px;
}

.search-results .sr-title {
  font-size: 1.02rem;
  line-height: 1.3;
  text-decoration: none;
  color: var(--primary);
}

.search-results .sr-title[target="_blank"]::after {
  content: " ↪";
  font-weight: 400;
}

.search-results .sr-snippet-col {
  flex: 1 1 60%;
}

.search-results .sr-snippet {
  color: var(--secondary);
  font-size: 0.95rem;
  line-height: 1.5;

  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

@media (max-width: 500px) {
  .search-results li {
    flex-direction: column;
    gap: 8px;
  }

  .search-results .sr-title-col {
    max-width: none;
  }
}
Built with Hugo, Powered by Github.
全站约 386 篇文章 合计约 1096216 字