先打个广告:欢迎关注我的公众号,参与 文史大挑战 趣味题目。使用方法见 这篇文章 。
正文开始:
我的网站使用 Hugo 编译静态页面。在此基础上,支持了搜索和评论等动态内容。我打算分两篇文章介绍具体的实现方法,分享给读者。这是第二篇。
本文先介绍评论功能的实现方法,还有 另一篇文章 介绍如何添加搜索。
效果预览
评论效果如下:
先决条件
关于搭建静态网站的做法,我在 这篇文章 中有简单介绍。要在静态网站添加动态内容,有如下一些条件:
- 自己掌握的云服务器
- 反向代理(比如nginx)
- 数据库(比如 mysql 或 redis )
具备以上的条件后,我们接下来可以了解下流程。相比搜索功能,增加评论的步骤会简单一些。
基本原理
基本的流程是:
- 设计一个用于搜索的路由名称,不能跟 hugo 项目已有的静态路由冲突,比如我用的
/comment/
- 在 Hugo 项目的每篇文章页面模板中增加评论的列表和表单
- 在 Hugo 项目中,在每篇文章页面中加在 JavaScript 文件,用于向后台请求评论内容。
- 建立后台搜索服务,从数据库中提取评论内容。
- 在 nginx 配置中,将来自
/comment/
的请求,分发给后台搜索服务
简单的部署图如下:
下面分步骤介绍实现方法:
制定路由
路由的名称不能和 Hugo 项目的 content 目录下的内容重复,会被 Hugo 和 nginx 同时使用。我这里使用/comment/
.
创建评论列表和表单
评论列表和表单的 HTML 代码例子如下:
<h2>精彩评论</h2>
<div class="border-0 divide-y mb-6" id="comment-container"></div>
<div class="border-0 w-full pt-4" id="comment">
<textarea class="text-sm my-1 border-1 divide-y leading-6 border border-gray-400 w-full p-2" id="comment-content" name="comment-content" placeholder="必填" minlength="1" maxlength="1000" rows="5"></textarea>
<div class="flex w-full flex-row flex-nowrap justify-between items-end md:justify-start md:items-center ">
<div class="my-2 ">
<label class="mr-1" for="comment-nick">称呼</label>
<input class="h-5 w-40 md:w-auto border border-gray-300 px-1" id="comment-nick" placeholder="必填" name="comment-nick" autocomplete="on" maxlength="25">
</div>
<div class="mx-4" id="comment-prompt">
<button id="comment-submit" style='color:rgba(200,200,200,1)' disabled class="h-5 border border-gray-400 justify-self-stretch rounded border w-24 px-2 my-2 hover:text-eureka">提交</button>
</div>
</div>
</div>
注意上面的代码中,id 为 comment-container
的 div
是评论列表,将来由 Javascript 请求回来并填充;id 为 comment
的 div
则是提交评论的列表。
为了维护方便,上面这段代码不必直接放入文章页面的模板内,而是单独作为一个 partial 保存,例如保存在 layouts/partials/components/comment.html
文件里,再由单个文章页面的模板引用。
单个文章页面的模板文件在 Hugo 项目的 layouts/_default/single.html
。编辑这个文件,在相关位置引用上面的 comment.html
:
{{ with .Page.Params.comment }}
{{ partial "components/comment" . }}
{{ end }}
.Page.Params.comment
参数是在 Frontmatter 里增加的一个开关,用于控制当前这篇文章是否启用评论功能。
此处省略页面元素的样式属性,因为这取决于各个网站的视觉风格。
这里没有使用 <form>
表单语法,而使用 Vanilla Javascript 提交评论内容
页面发起请求
新建一个 JavaScript 文件,不妨命名为 comment.js
,放在 static
目录下。因为评论列表位置比较靠后,我希望在页面元素全部加载完后再执行这个 js 文件,所以放在 </body>
之前,具体位置是 layouts/_default/baseof.html
文件,增加如下语句:
{{- if .IsPage }}
{{- $assets := .Site.Data.assets }}
<script defer src="{{ $assets.base64.js.url }}"></script>
<script defer src="{{ $assets.comment.js.url }}"></script>
{{- end }}
$assets
变量是定义在 data/assets.yaml
中,用于单独保存 js 文件名,效果如下:
base64:
js:
url: /js/lib/base64.js
comment:
js:
url: /js/lib/comment.js
base64.js
文件用于压缩加密 URL 使用,让 URL 更简洁。
comment.js
文件的关键内容有:
var btn = document.getElementById('comment-submit');
// 请求评论内容
function main(){
// 将提交按钮绑定为发送评论
if (btn !== null){
btn.addEventListener('click', function(){
var data = {
path: window.location.pathname.trim(),
nick: nick.value.trim() || '',
content: content_area.value.trim(),
}
var r = new XMLHttpRequest();
r.addEventListener('load', function () {
var result = JSON.parse(r.responseText)
var prompt = document.getElementById('comment-prompt');
if(result.status === 'ok' && prompt !== null){
prompt.innerHTML = '感谢您的评论,将在审核后公布'
}
});
r.open("POST", "/comment/", true);
r.setRequestHeader('content-type', 'application/json')
r.send(JSON.stringify(data));
btn.setAttribute('disabled' ,'')
hide(btn);
})
}
// 加载评论列表
if(window.location.pathname.length > 0){
var r = new XMLHttpRequest();
r.addEventListener('load', function () {
var result = JSON.parse(r.responseText)
if(result.status === 'ok'){
populate(result)
}
});
r.open("GET", "/comment/"+BASE64.urlsafe_encode(window.location.pathname), true);
r.send(null);
}
}
function populate(result){
var comment_container = document.getElementById('comment-container');
var divider = times('-', 100);
if (comment_container !== null){
// 显示评论列表
}else{
comment_container.innerText = '暂无评论,欢迎您在下方留言'
}
}
window.onload = main
接下来开始处理后台数据,包括评论的保存和读写处理。
设计数据库
要实现高效的搜索,需要使用数据库。这里以 mysql 数据库为例。
安装好 mysql 后,需要首先创建库和表。为了保存搜索结果的来源信息,数据表需要存储文章的所属 section、标题 等信息。
所以表格设计如下:
+----+---------+---------------------------------+--------------------------+--------------+---------------------+--------+
| id | section | title | content | nick | time | state |
+----+---------+---------------------------------+--------------------------+--------------+---------------------+--------+
| 2 | tech | pull-docker-images-behind-proxy | 找了好久,终于找到想要的 | tony | 2023-01-09 15:55:27 | show |
+----+---------+---------------------------------+--------------------------+--------------+---------------------+--------+
section 和 tech 字段用于拼接文章的链接,title 字段用于显示,content 是文章的内容,用于搜索。
建立后台评论读写服务
简单的方案使用 Flask 框架建立服务,这个话题可以另开一篇,这里只列出视图函数最关键的部分:
# comment 是个蓝图,公共前缀是 /comment
# 读取评论条目
@comment.route('/<string:code>', methods=['GET'])
def index(code):
# 省略变量准备和异常处理的部分
c = Comment.query.filter_by(section=section, title=title).all()
comments = [
{
'id':x.id,
'content':x.content,
'nick':x.nick,
'time':datetime.datetime.strftime(x.time, '%Y-%m-%d %H:%M')
} for x in c if x.state == 'show']
resp = {'output': comments, 'status':'ok'}
status_code = 200
return jsonify(resp), status_code
# 写入评论条目
@comment.route('/', methods=['POST'])
def add_comment():
# 省略变量准备和异常处理的部分
c = Comment(
section=section,
title=title,
nick=nick,
content=content,
time=datetime.datetime.now()
)
db.session.add(c)
db.session.commit()
resp = {'output': repr(c), 'status':'ok'}
return jsonify(resp), status_code
另外再配合安装 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 静态内容配合使用的动态评论功能。
如果您对本文有疑问或者寻求合作,欢迎 联系邮箱 。邮箱已到剪贴板
精彩评论
本站 是个人网站,采用 署名协议 CC-BY-NC 授权。
欢迎转载,请保留原文链接 https://www.lfhacks.com/tech/dynamic-content-on-static-site-2/ ,且不得用于商业用途。