Lawtee News

给HUGO添加文章搜索功能

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

n

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

n

content 目录下新建 search.md 页面

n

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

nnnnnn

1---n2title: "搜索"n3date: 2025-11-20T00:00:00+08:00n4type: "search"n5layout: "search"n6---n7在此搜索本博客文章。

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

n

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

nnnnnn

1[outputs]n2  home = ["HTML", "RSS", "JSON"]  # 添加 JSON 输出n3n4[outputFormats.JSON]n5  baseName = "index"n6  mediaType = "application/json"

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

n

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

nnnnnn

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

layouts 中新建 index.json 模板

n

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

nnnnnn

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

其他 CSS 配置

n

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

nnnnnn

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