惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tor Project blog
AI
AI
S
Securelist
P
Privacy International News Feed
A
Arctic Wolf
T
Tenable Blog
C
Cisco Blogs
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
AWS News Blog
AWS News Blog
L
Lohrmann on Cybersecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
S
Security Affairs
P
Palo Alto Networks Blog
K
Kaspersky official blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园 - 叶小钗
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - Franky
SecWiki News
SecWiki News
IT之家
IT之家
G
GRAHAM CLULEY
酷 壳 – CoolShell
酷 壳 – CoolShell
C
CERT Recently Published Vulnerability Notes
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
月光博客
月光博客
H
Help Net Security
P
Proofpoint News Feed
Cloudbric
Cloudbric
Latest news
Latest news
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Schneier on Security
Schneier on Security
Help Net Security
Help Net Security
Apple Machine Learning Research
Apple Machine Learning Research
Webroot Blog
Webroot Blog
B
Blog
量子位
J
Java Code Geeks
MyScale Blog
MyScale Blog

晚花行乐

马克卡尼在2026年达沃斯论坛上的讲话(阅读材料) | 晚花行乐 鸡娃如何用力才是恰到好处 | 晚花行乐 读万卷书,行万里路的辩证关系 | 晚花行乐 反对培训机构掐尖招生 | 晚花行乐 小泽和建国会谈最后10分钟全文(阅读材料) | 晚花行乐 来看看 DeepSeek 怎么鸡娃 | 晚花行乐 谈谈基本功 | 晚花行乐 惠普 ProDesk SFF PC 各系列参数对比 | 晚花行乐 解决瘦客户机上安装 Debian 12 启动失败问题 | 晚花行乐 意拾喻言:老外写的文言文 | 晚花行乐 笠翁对韵中的典故(十三元) | 晚花行乐 谈谈中考取消四小门 | 晚花行乐 亚马逊云科技产品免费试用攻略(3) - 对象存储服务 | 晚花行乐 在 Windows 10 LTSC 版本上安装 WSL2 | 晚花行乐 Debian 12 的常用配置项 | 晚花行乐 在 Debian 12 上安装 Nvidia 显卡驱动程序 | 晚花行乐 解决 Debian 12 关机失败问题 | 晚花行乐 解决 VS Code 自动更新版本后卡在连接界面 | 晚花行乐 观看巴黎奥运会有感 | 晚花行乐 在 Windows10 上安装惠普旧打印机驱动程序 | 晚花行乐 欢迎关注公众号:晚花行乐 | 晚花行乐 如何编写拼写检查器 | 晚花行乐 亚马逊云科技产品免费试用攻略(2) - 云服务器 | 晚花行乐 Pandas 中 axis 参数的理解(附实例) | 晚花行乐 我打算命个名,叫什么什么 Manager | 晚花行乐 上海武康路历史建筑一览 | 晚花行乐 Python 实现简单的数学表达式解析并处理 | 晚花行乐 观看马拉松的感悟 | 晚花行乐 Python 保存 Cookies 到文件并再次读取 | 晚花行乐 如何为 Hugo 静态网站添加评论功能 | 晚花行乐 Linux 共享打印服务 CUPS | 晚花行乐 解决 CSV 文件的第一列不能解析 | 晚花行乐 亚马逊云科技产品免费试用攻略(1) - 注册账户 | 晚花行乐 第二幕 Atma 的闲聊 | 晚花行乐 第二幕野蛮人的语音 | 晚花行乐 古入声和普通话平声对照 | 晚花行乐 第一幕的背景音乐 | 晚花行乐 第二幕亚马逊的语音 | 晚花行乐 第一幕的亚马逊的语音 | 晚花行乐 第一幕的野蛮人的语音 | 晚花行乐 MacOS 的彩蛋:Here's to the crazy ones | 晚花行乐 笠翁对韵的基本知识 | 晚花行乐 笠翁对韵中的典故(十二文) | 晚花行乐 笠翁对韵中的典故(十一真) | 晚花行乐 笠翁对韵中的典故(十灰) | 晚花行乐 杭州景点的楹联 | 晚花行乐 adb keycode 大全 | 晚花行乐 Scikit-learn 学习笔记(0)名词术语 | 晚花行乐 Scikit-learn 学习笔记(3)监督学习的例子 | 晚花行乐 SQLite 文档的学习笔记(1)长期支持计划 | 晚花行乐 SQLite 文档的学习笔记(2)测试方法 | 晚花行乐 笠翁对韵中的典故(九佳) | 晚花行乐 Ansible 如何检查一个程序的版本 | 晚花行乐 Ansible 如何检查一个文件夹是否存在 | 晚花行乐 pip 配置文件详解 | 晚花行乐 Ansible 如何检查一个URL是否正常 | 晚花行乐 Ansible 如何修改 iptables 规则 | 晚花行乐 Ansible 指定 playbook 运行的主机 | 晚花行乐 Ansible 如何清空文件夹 | 晚花行乐 Ansible 如何在本机执行命令 | 晚花行乐 笠翁对韵中的典故(八齐) | 晚花行乐 Python 中 Defaultdict 的理解 | 晚花行乐 《伊索寓言》电子书 | 晚花行乐 菲伯尔钢琴伴奏:第二册 | 晚花行乐 Python 的 Keyword-Only Arguments 理解 | 晚花行乐 Python 的 函数参数处理机制 | 晚花行乐 瓦瑞夫在第一幕的闲聊 | 晚花行乐 瓦瑞夫在第一幕的任务提示 | 晚花行乐 第一幕的女巫语音 | 晚花行乐 《Fluent Python》 读书笔记:文本和字节序列 | 晚花行乐 第一幕的罗格语音 | 晚花行乐 第一幕的圣骑士语音 | 晚花行乐 第一幕的男巫语音 | 晚花行乐 第一幕的旁白 | 晚花行乐 第一幕的恶魔 | 晚花行乐 Python 中 Element Tree 的理解 | 晚花行乐 《Fluent Python》 读书笔记:字典和集合 | 晚花行乐 笠翁对韵中的典故(七虞) | 晚花行乐 笠翁对韵中的典故(五微) | 晚花行乐 笠翁对韵中的典故(六鱼) | 晚花行乐 笠翁对韵中的典故(一东) | 晚花行乐 笠翁对韵中的典故(二冬) | 晚花行乐 笠翁对韵中的典故(三江) | 晚花行乐 笠翁对韵中的典故(四支) | 晚花行乐 姜太公钓鱼 | 晚花行乐 武王建立周朝 | 晚花行乐 大禹治水 | 晚花行乐 尧舜让位 | 晚花行乐 黄帝战蚩尤 | 晚花行乐 上下五千年-精简版 | 晚花行乐 成功修复鼠标按键 | 晚花行乐 横向的Word文档怎么加页眉页脚 | 晚花行乐 商标通用化的故事:商标代替商品名 | 晚花行乐 搜索空文件夹的批处理程序 | 晚花行乐 Sn0wbreeze不能运行? | 晚花行乐 天线的驻波比 | 晚花行乐 天线参数:增益Gain | 晚花行乐 天线参数:方向图Radiation pattern | 晚花行乐 本拉登别墅的Google Earth坐标 | 晚花行乐 宜家帕克斯(PAX)衣柜的拼装过程 | 晚花行乐
如何为 Hugo 静态网站添加搜索功能 | 晚花行乐
2023-11-11 · via 晚花行乐

我的网站使用 Hugo 编译静态页面。在此基础上,支持了搜索和评论等动态内容。我打算分两篇文章介绍具体的实现方法,分享给读者。

本文先介绍搜索功能的实现方法,还有 另一篇文章 介绍如何添加评论。

效果预览

搜索效果如下:

先决条件

关于搭建静态网站的做法,我在 这篇文章 中有简单介绍。要在静态网站添加动态内容,有如下一些条件:

  • 自己掌握的云服务器
  • 反向代理(比如nginx)
  • 数据库(比如 mysql 或 redis )

具备以上的条件后,我们接下来可以了解下流程。

基本原理

基本的流程是:

  1. 制定一个用于搜索的路由,不能跟 hugo 项目已有的路由冲突,比如我用的 /s/
  2. 在 Hugo 项目的页面中创建请求搜索的表单,指向 /s/
  3. 在 Hugo 项目中,content 目录中建立名叫 s 的静态内容,作为搜索结果的底版页面。
  4. 在 Hugo 项目中,在搜索页面中加在 JavaScript 文件,用于向后台请求搜索内容。
  5. 在 Hugo 项目中,配置 输出文件格式 ,增加一种 txt 格式输出,输出内容为每篇文章的无格式内容。
  6. 编写后台脚本,用于在 Hugo 项目每次编译后,将输出的 txt 内容写入数据库
  7. 建立后台搜索服务,从数据库中查找关键字
  8. 在 nginx 配置中,针对 /s/ 的请求,分发给后台搜索服务

简单的部署图如下:

下面分步骤介绍实现方法:

制定路由

路由的名称不能和 Hugo 项目的 content 目录下的内容重复,会被 Hugo 和 nginx 同时使用。我这里使用/s/.

创建表单

页面顶部的表单效果如下:

( ☝ 页面顶栏效果 )

页面顶栏的文件在 Hugo 项目的 layouts/partials/header.html。编辑这个文件,在相关位置添加如下的表单:

<form method='GET' action='/s' class="" id="search-keyword">
  <input class="..." id="search-input" maxlength="20" name="s" placeholder="" autocomplete="off" >
  <button class="..." type="submit">{{ i18n "search" . }}</button> 
</form>

此处省略页面元素的样式属性,因为这取决于各个网站的视觉风格。

这里有两点要注意:

  1. 表单的请求方法为 GET
  2. 表单的 action 属性为事先指定的路由 /s/

这个表单填入关键字(比如 李白 )后,会转到 /s/?s=李白 这个链接。后面会处理这个链接。

建立目录

在 Hugo 项目中,content 目录中建立与路由同名(我这里是s)的静态内容,作为搜索结果的底版页面。

比如,我的 content 目录的结构如下:

content
  ├─ archive
  ├─ blog
  ├─ game
  ├─ grow
  ├─ nature
  ├─ ...
  └─ s
     └─ _index.md

这个 s 页面将被 Hugo 编译并形成一个静态页面,所以需要为这个页面设计一个空的 index 页面 (_index.md),作为搜索结果的页面。搜索结果的内容将在后面章节中使用 Javascript 填充。

index 页面 (_index.md) 的例子:

---
title: 搜索结果
headless: true
---

在 front matter 中设置 headless:true 的原因是:不希望这个页面作为一个普通的页面被统计到归档中去,也不产生其他类型的输出文件。

搜索结果页面样式

在上节中创建的静态页面 /s/,需要一个对应的列表的样式文件,用于指定搜索结果列表的样式。

可以写在 layouts/s/list.html 中。内容举例如下:

{{ define "main" }}
  <article class="mx-6 my-8">
    <h1 class="text-3xl text-primary-text" id="search-title">{{ .Title }}</h1>
    {{ with .Content }}
      <div class="content mt-4 text-secondary-text">
        {{ . }}
      </div>
    {{ end }}
  </article>
    
  <div class="bg-secondary-bg rounded px-6" id="search-result">
    <div class="overflow-hidden divide-y">
    </div>
  </div>
{{ end }}

注意!上面的样式仅仅是例子,需要读者根据自己的网站的特点设计样式。上面 id#search-result<div>内容是空的,将在下节中使用 JavaScript 填充。

页面发起请求

接下来开始处理 /s/?s=李白 页面向后台发出请求,这个用 Javascript 实现。

由于/s/是静态页面,所以这个 URL 中的 querystring 部分,在正常情况下,不会有没有任何一方来处理。所以可以使用 JavaScript 处理。

如果要让 Hugo 页面加载 js,需要处理两个地方,一个是 layouts/partials/head.html 文件,另一个是新建一个 JavaScript 文件,不妨命名为 search.js ,放在 static 目录下,

head.html 文件里加载 js 的部分,增加如下语句:

{{- $assets := .Site.Data.assets }}

{{- if eq .RelPermalink "/s/" }}
  <script defer src="{{ $assets.base64.js.url }}"></script>
  <script defer src="{{ $assets.search.js.url }}"></script>
{{- end }}

{{ .Site.Data.assets }} 变量指 data/assets.yaml 文件,保存着 search.js 文件的路径。

{{if}} 判断用来确保只有 /s/ 搜索页面才加载这个 js 文件。

search.js 文件的作用主要是将 querystring 编码为 base64,然后向后台发请求。关键的部分如下:


// 处理 querystring
decodeURIComponent(window.location.search.substring(3));

// 填充搜索结果
if(search_title !== null){
  if (result.output.length > 0){
      search_title.innerHTML = '有关 "'+result.keywords.join(', ') + '" 的 '+result.output.length.toString()+' 条搜索结果'
  }else{
      search_title.innerHTML = '没有找到 "'+result.keywords.join(', ') + '" 的匹配结果'
  }
}

// 向后台发请求
var r = new XMLHttpRequest();

r.addEventListener('load', function () {
    var result = JSON.parse(r.responseText)
    if(result.status === 'ok'){
        populate(result)
    }
});
r.open("GET", "/search/"+BASE64.urlsafe_encode(query), true);
r.send(null);

// 加载页面时调用函数
window.onload = getSearchResult

接下来开始处理后台数据,包括内容数据和后台服务。

输出纯文本格式的文章内容

Hugo 编译输出的产物,除了常规的静态 HTML 格式以外,还可以输出多种 MIME type 的文件。具体的格式列表 在这里

为了实现搜索功能,希望只得到每篇文章的文字,而不包括 front matter 和 markdown 的格式信息。先简单了解下 Hugo 输出的逻辑:

上图中,蓝色方块在 配置文件中枚举出,分别对应 outputFormatsoutputs,绿色方块在 layouts 目录中出现,黄色的输出则在应该在的地方(取决于来源 partial 的位置,比如首页位置、section位置、每个页面的位置)

下面是具体的实现方法:

首先,修改配置文件(config/_default/config.yaml),增加一种输出格式。在 yaml 格式的最顶层,找到 outputFormats,如果没有就创建一个。像下面这样:

outputFormats:
  content:
    mediaType: "text/plain"
    baseName: "plain-page-content"
    isPlainText: true

这样增加了一类名叫 content 的输出格式,参数的含义是:增加的这种输出,采用 “text/plain” 的媒体类型,输出文件的 base name 是 plain-page-content,输出文件的扩展名跟随模板文件的扩展名(这里是 .txt)。在每次编译完成后,我们将在每篇文章的目录中得到 plain-page-content.txt 这个输出文件。

然后,继续修改配置文件,增加一种输出结果,采用上面定义的 content 输出格式。

outputs:
  page:
    - "html"
    - "content"

page 中,原有的 html 是默认的渲染输出,再增加一种 content

最后,创建输出格式的模板文件,命名规则 在这里

简单的说,就是 【内容来源】.【输出格式名称】.【输出文件类型】

  • 搜索的内容来源来自各个 single页面
  • 输出格式使用上面增加的 content 类型;
  • 输出文件类型可以自由发挥,我们使用最普通的 txt

所以就得到了输出模板的名称:single.content.txt,放置在 layouts/_default/ 目录下。

这个模板的作用,就是将每个页面的 Page 变量,通过模板的格式化,得到渲染的结果。

为了加深理解,看看 layouts/_default/ 目录下的 single.html 文件,它对应的是 config 文件中的 outputs.page.html 输出。

为了得到去掉格式的纯文本,single.content.txt 文件内容如下:

{{ .Plain | htmlUnescape }}

输出纯文本格式的文章标题

用相同的办法,同时输出纯文本格式的标题。具体配置如下:

配置文件(config/_default/config.yaml),增加一种输出格式:

outputFormats:
  content:
    mediaType: "text/plain"
    baseName: "plain-page-content"
    isPlainText: true
  title:
    mediaType: "text/plain"
    baseName: "plain-page-title"
    isPlainText: true

增加一种输出结果,已有的配置不要删除,只增加就行

outputs:
  page:
    - "html"
    - "content"
    - "title"

新建模板:single.title.txt,放置在 layouts/_default/ 目录下,内容为:

{{ print .Title }}

到这里,每次编译结束后,在每篇文章的目录下应该输出两个文件:plain-page-content.txtplain-page-title.txt,内容分别是文章的内容和标题。到这里, Hugo 的改造就结束了,下面要开始编写外部的服务。

设计数据库

要实现高效的搜索,需要使用数据库。这里以 mysql 数据库为例。

安装好 mysql 后,需要首先创建库和表。为了保存搜索结果的来源信息,数据表需要存储文章的所属 section、标题 等信息。

所以表格设计如下:

+----+---------+-------------------------+--------------------------------+--------------+
| id | section | path                    | title                          | content      |
+----+---------+-------------------------+--------------------------------+--------------+
|  1 | tech    | mocha-reporter          | 为 Mocha.js 编写自定义的 reporter |    文        |
|  2 | tech    | puppeteer-wait-for-ajax | Puppeteer 等待页面更新            |    章        |
|  3 | tech    | python-namedtuple       | Python 中 NamedTuple 的理解      |    内        |
| .. | ...     | ...                     | ...                             |    容        |
+----+---------+-------------------------+---------------------------------+--------------+

建表语句为:

CREATE TABLE IF NOT EXISTS `search`(    
    `id` INT UNSIGNED AUTO_INCREMENT,
    `section` VARCHAR(64) NOT NULL,
    `path` VARCHAR(255) NOT NULL,
    `title` VARCHAR(128) NOT NULL,
    `content` TEXT NOT NULL,
    PRIMARY KEY ( `id` ) 
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

section 和 tech 字段用于拼接文章的链接,title 字段用于显示,content 是文章的内容,用于搜索。

编写读取文本文件到数据库的脚本

接下来要编写脚本用来把 Hugo 输出的文本文件(plain-page-content.txtplain-page-title.txt)的内容写入数据库。

对于 mysql 数据库,可以使用 Python 语言的 pymysql 库完成这个任务。有一个陷阱是在读入文件内容后,插入数据库之前,需要处理字符转义:

# 部分语句
from pymysql.converters import escape_string

# 文件内容读入 content
article = escape_string(content)

# 把 article 插入数据库
sql = f'INSERT INTO search(section, path, title, content) values ("{section}", "{path}", "{title}", "{article}")'   

建立后台搜索服务

简单的方案使用 Flask 框架建立服务,这个话题可以另开一篇,这里只列出视图函数最关键的部分:

import base64
import urllib
from app.models import Search
from sqlalchemy import and_

import regex as re

# search 是个蓝图,公共前缀是 /search
@search.route('/<string:code>', methods=['GET'])
def search(code):
  b_keyword = base64.urlsafe_b64decode(code)
  keyword = b_keyword.decode(encoding='utf-8', errors='strict')
  unquoted = urllib.parse.unquote(keyword)
  keywords = set([x.strip() for x in unquoted.split('+') if x])

  if keywords:
      criteria = [Search.content.like(f'%{w}%') for w in keywords]
      records = Search.query.filter(and_(*criteria)).limit(max_results).all()

另外再配合安装 gunicorn 和 supervisord 就可以开启 web 服务,这里假设web 服务开在 8888 端口。

配置 nginx

需要配置 nginx ,将 /s/ 的请求转发到后台 web 服务上,而其余的请求仍然解释为静态文件:


location ~ /search/ {
        proxy_pass http://127.0.0.1:8888;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

这样就实现了 Hugo 静态内容配合使用的动态搜索功能。

各位读后有什么想法,请在下方留言吧!如果对本文有疑问或者寻求合作,欢迎 联系邮箱邮箱已到剪贴板

精彩评论