老T博客

给HUGO添加文章搜索功能

给HUGO添加文章搜索功能

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

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

content 目录下新建 search.md 页面

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

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

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

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

1[outputs]
2  home = ["HTML", "RSS", "JSON"]  # 添加 JSON 输出
3
4[outputFormats.JSON]
5  baseName = "index"
6  mediaType = "application/json"

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

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

  1{{ define "main" }}
  2<content>
  3  <h1>{{ .Title }}</h1>
  4  <div class="search-box">
  5    <input
  6      id="search-input"
  7      class="search-input"
  8      type="text"
  9      placeholder="输入关键词搜索…"
 10      autocomplete="off"
 11    />
 12  </div>
 13
 14  <ul id="results" class="search-results">
 15    <li style="color:#666">请输入关键词搜索</li>
 16  </ul>
 17
 18</content>
 19
 20<script>
 21// ========== 工具函数 ==========
 22function escapeHtml(str) {
 23  if (!str) return "";
 24  return str
 25    .replace(/&/g, "&amp;")
 26    .replace(/</g, "&lt;")
 27    .replace(/>/g, "&gt;");
 28}
 29
 30// ========== 精确搜索 ==========
 31function exactMatch(haystack, needle) {
 32  if (!haystack || !needle) return false;
 33  return haystack.toLowerCase().includes(needle.toLowerCase());
 34}
 35
 36// ========== 渲染结果 ==========
 37function renderResults(list) {
 38  const resultsEl = document.getElementById("results");
 39
 40  if (!list || list.length === 0) {
 41    resultsEl.innerHTML = '<li style="color:#666">未找到结果。</li>';
 42    return;
 43  }
 44
 45  const itemsHtml = list.map(item => {
 46    const title = escapeHtml(item.title || "(无标题)");
 47
 48    // 外链或本地链接
 49    const url = escapeHtml(item.link || item.url || "#");
 50    const isExternal = !!item.link;
 51
 52    const linkAttrs = isExternal
 53      ? ' target="_blank" rel="noopener noreferrer"'
 54      : "";
 55
 56    // 摘要:取 summary 或 content 开头一段
 57    const summaryRaw =
 58      item.summary ||
 59      (item.content ? item.content.slice(0, 200) + "…" : "");
 60
 61    const summary = escapeHtml(summaryRaw);
 62
 63    return `
 64      <li>
 65        <div class="sr-title-col">
 66          <a class="sr-title" href="${url}"${linkAttrs}>${title}</a>
 67        </div>
 68
 69        <div class="sr-snippet-col">
 70          <div class="sr-snippet">${summary}</div>
 71        </div>
 72      </li>
 73    `;
 74  }).join("");
 75
 76  resultsEl.innerHTML = itemsHtml;
 77}
 78
 79// ========== 加载 index.json 数据 ==========
 80async function loadIndex() {
 81  try {
 82    const res = await fetch("/index.json");
 83    return await res.json();
 84  } catch (err) {
 85    console.error("加载 index.json 失败:", err);
 86    return [];
 87  }
 88}
 89
 90// ========== 主逻辑 ==========
 91(async function () {
 92  const data = await loadIndex();
 93  const input = document.getElementById("search-input");
 94
 95  input.addEventListener("input", () => {
 96    const q = input.value.trim();
 97
 98    if (!q) {
 99      renderResults([]);
100      return;
101    }
102
103    // 精确搜索:title / content / tags 均可匹配
104    const result = data.filter(item =>
105      exactMatch(item.title, q) ||
106      exactMatch(item.content, q) ||
107      (item.tags || []).some(t => exactMatch(t, q))
108    );
109
110    renderResults(result);
111  });
112})();
113</script>
114{{ end }}

layouts 中新建 index.json 模板

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

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

其他 CSS 配置

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

  1/* ====== 搜索框区域布局 ====== */
  2.search-box {
  3  max-width: 720px;
  4  margin: 24px 0 32px;
  5}
  6
  7.search-input {
  8  width: 100%;
  9  padding: 10px 14px;
 10  font-size: 16px;
 11
 12  border: 1px solid var(--border);
 13  border-radius: 8px;
 14
 15  background: var(--entry);
 16  color: var(--primary);
 17
 18  outline: none;
 19  transition: border-color .15s ease, box-shadow .15s ease;
 20}
 21
 22.search-box {
 23  max-width: 720px;
 24  margin: 24px 0 32px;
 25}
 26
 27.search-input {
 28  width: 100%;
 29  padding: 10px 14px;
 30  font-size: 16px;
 31
 32  border: 1px solid rgba(150, 150, 150, 0.35);
 33  border-radius: 8px;
 34
 35  background: var(--entry);
 36  color: var(--primary);
 37  outline: none;
 38
 39  transition: border-color .15s ease, box-shadow .15s ease;
 40}
 41
 42.search-input:focus {
 43  border-color: var(--text-highlight);
 44  box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.20);
 45}
 46
 47.search-results {
 48  list-style: none;
 49  padding: 0;
 50  margin: 0;
 51}
 52
 53.search-results li {
 54  display: flex;
 55  align-items: flex-start;
 56  gap: 16px;
 57  padding: 12px 0;
 58  border-bottom: 1px solid rgba(0,0,0,0.04);
 59}
 60
 61.search-results .sr-title-col {
 62  flex: 0 0 40%;
 63  min-width: 180px;
 64  max-width: 420px;
 65}
 66
 67.search-results .sr-title {
 68  font-size: 1.02rem;
 69  line-height: 1.3;
 70  text-decoration: none;
 71  color: var(--primary);
 72}
 73
 74.search-results .sr-title[target="_blank"]::after {
 75  content: " ↪";
 76  font-weight: 400;
 77}
 78
 79.search-results .sr-snippet-col {
 80  flex: 1 1 60%;
 81}
 82
 83.search-results .sr-snippet {
 84  color: var(--secondary);
 85  font-size: 0.95rem;
 86  line-height: 1.5;
 87
 88  overflow: hidden;
 89  display: -webkit-box;
 90  -webkit-line-clamp: 3;
 91  -webkit-box-orient: vertical;
 92}
 93
 94@media (max-width: 500px) {
 95  .search-results li {
 96    flex-direction: column;
 97    gap: 8px;
 98  }
 99
100  .search-results .sr-title-col {
101    max-width: none;
102  }
103}

#hugo博客 #搜索功能

评论