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

推荐订阅源

GbyAI
GbyAI
N
News and Events Feed by Topic
D
DataBreaches.Net
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
T
Tailwind CSS Blog
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
Y
Y Combinator Blog
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
A
About on SuperTechFans
I
InfoQ
S
Securelist
Last Week in AI
Last Week in AI
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
腾讯CDC
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
博客园 - 三生石上(FineUI控件)
Simon Willison's Weblog
Simon Willison's Weblog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
美团技术团队
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
罗磊的独立博客
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Latest news
Latest news
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 热门话题
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队

晚花行乐

马克卡尼在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 静态内容配合使用的动态搜索功能。

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

精彩评论