背景
这几周的工作忙得要命,但本着越忙就越要忙里偷闲续命的原则,这两天抽空研究了下怎么给博客接入评论系统。
实际上在这个博客的建立之初(其实这个博客已经重启过一次了,我指的是第一次建立)就有过这个念头,但当时以私人博客没人看 + 想保持简单为由否决了。
随着时间推移,虽然第一个原因还是没有变化,但第二个原因已经渐渐没有那么满足了。换言之,在几年的使用后,我对这个博客的印象从简单变得有点偏向简陋了,开始希望它能有点新花样。可能这就类似老夫老妻——算了。
说回博客本身,这个博客的内容是写在Obsidian里的,使用了Digital Garden插件发布到这个站点。整个流程其实很直白:
- 在github上fork Digital Garden的仓库,部署到Vercel上,也就是这个站点。这个仓库里只有一个空data目录,以及一套渲染这个data目录里文档的壳子。
- 在Obsidian里,使用Digital Garden插件的命令,将写好的文档推送到上面说的github仓库的data里。Vercel通过监听该仓库的commit触发自动部署,网站就上线了。
也是从搭建这个博客开始,一直在非互联网公司当井底之蛙做传统企业应用开发的我首次亲手触摸到了Serverless、Serverless Function和SaaS。虽然依旧是作为使用者而非开发者,但整体来说还是一种很新奇的体验。
如果是几年前,当我想给自己的博客加一个评论系统,第一个念头肯定是换一个自带评论系统的博客系统,例如WordPress,其次是自己在当前的静态博客里自行实现。但经过上面说的第一次接触,现在的我第一反应是找一个评论系统提供商——如果能支持self-host就更好了。
这么看来,在专业技术领域,这个博客也并非一无是处嘛。
选型
回到话题本身,经过一番Google,我大概意向锁了这几个评论系统:
简单的比较后,先排除掉了看起来略显老旧的Commento,和docker镜像很久都没更新的Cusdis。不得不说,docker就像钟离的盾,养出了我们这种Linux废人。
Isso功能上非常克制:

Remark42就齐全一些:

克制和丰富都是我喜欢的,在纠结了很久之后,最终因为自带深色主题而选择了后者。这个抉择点是我一开始没想到的。
一次并不简单的集成
出于职业习惯,虽然这个事情看上去并不麻烦,但在开始前还是要梳理一下要做哪些事。
首先,肯定要在我的云服务器上部署一个Remark42,看文档它支持docker-compose,那这一步应该很简单,然后将它暴露在公网上。
其次,在我github上Digital Garden的博客仓库里——虽然我对前端一窍不通,但应该可以照猫画虎——给每个页面嵌入Remark42提供的script,指向第一步中部署的Remark42服务端。
这个看似简单的流程,在一些因为知识缺乏而始料未及的问题,和我个人偏好的影响下,实际上并没有那么简单。
最主要的问题就是这个博客使用了Vercel的子域名,是https的,而我暴露在公网的Remark42地址是http://ip:port,浏览器会拒绝从https的环境里调用http请求。
一个比较简单的方案是加上允许不安全请求的配置,但我时间比较充裕,不是很想这么做。
另一个方案是给我的云服务器一个域名,把Remark42挂在这个域名上。我确实有个域名,但属实不想去折腾备案。本来对着shell敲敲命令就能解决的问题,等审核就不知道要多久了。我时间充裕,但也没那么充裕,最多就是周六一天,周日还有事。
至于自签IP证书,我没试,但大概是行不通的。
我最终选择了一条在折腾度和有趣程度之间取得了微妙平衡的路子:我手头刚好有一台香港的超低配云服务器,我可以把域名解析到它上面(域名解析到香港的云服务器不需要备案),然后让它把Remark42的流量转发到我的云服务器上。
接下来就分为Remark42服务器、Proxy服务器和前端三部分,简单记录一下我是怎么做的吧!
Remark42服务器
这台机器是我的主要云服务器,负责运行Remark42服务。它上面还有一些其他的服务,以及我一万年没有启动的Minecraft GTNH服务器。
Remark42支持docker-compose,配置其docker-compose.yaml:
version: "2"
services:
remark:
build: .
image: umputun/remark42:latest
container_name: "remark42"
hostname: "remark42"
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
ports:
- "57711:8080"
environment:
- REMARK_URL=https://remark42.cloudworks.space
- SITE=solitary-dream.vercel.app
- SECRET=
- STORE_BOLT_PATH=/srv/var/db
- BACKUP_PATH=/srv/var/backup
- AUTH_SAME_SITE=none
- ADMIN_SHARED_ID=
- AUTH_EMAIL_ENABLE=true
- NOTIFY_USERS=email
- NOTIFY_EMAIL_FROM=
- NOTIFY_ADMINS=email
- ADMIN_SHARED_EMAIL=
- SMTP_HOST=
- SMTP_PORT=
- SMTP_TLS=
- SMTP_USERNAME=
- SMTP_PASSWORD=
- AUTH_EMAIL_FROM=
volumes:
- ./var:/srv/var
具体配置参考官方文档,但不得不说他们这个文档真的写得稀烂。参数说明写得含糊不清,让使用者倾向于去猜,但问题是参数名称起得也不好,猜都没地方猜。
docker-compose up -d
这里可以先把57711放开到公网,访问http://ip:57711/web,看看Remark42提供的Demo页面是否OK,OK的话就说明部署没什么问题。
随后配置nginx,将57711反向代理到57712,自签一个SSL证书,暴露在公网,用于给Proxy机器调用。
server {
listen 57712 ssl;
listen [::]:57712 ssl;
server_name Remark42服务器本机IP;
ssl_certificate /path/to/crt;
ssl_certificate_key /path/to/key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:57711/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
校验 + 重新载入nginx配置:
nginx -t
nginx -s reload
证书的公钥传到Proxy机器上去,洗净备用。
Proxy服务器
Proxy服务器负责接收博客站点发往Remark42域名的请求,并将流量转发到上面的Remark42服务器。
首先配置Proxy服务器的域名解析。我的域名原本是在阿里云买的,后来转到了Cloudflare,配置如下:

也就是将remark42.cloudworks.space解析到119这台机器(Proxy服务器)上。
随后在nginx配置里新增remark42.cloudworks.space站点,使用Certbot配置其证书:
certbot --nginx
最后在配置完成的nginx.conf里,将发往Remark42服务器的流量使用上一步的公钥再次加密:
server {
server_name remark42.cloudworks.space;
location / {
proxy_pass https://Remark42服务器IP:57712/;
proxy_set_header Host Remark42服务器IP;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_ssl_trusted_certificate /path/to/crt;
}
# Certbot自动生成的配置略
}
同样校验并重新载入nginx配置。
此时访问https://remark42.cloudworks.space/web ,应该得到跟前面一样的Demo页面。
前端
正如前面所说,我对前端一窍不通,但好在Digital Garden文档写得不错,代码也很简单,这一步并没有遇到什么障碍。
根据文档,如果我要在每个页面的正文后面都加一个组件,需要将其代码写到src/site/_includes/components/user/common/beforeContent/fileName.njk下。
为什么是beforeContent而不是更符合直觉的afterContent呢,因为DigitalGarden的afterContent有问题。对于分段加载的长文档,它会将afterContent的内容展示在第一段下面,而非所有分段最后。
我没接触过这个njk文件,但他们官网说可以兼容原生HTML,那你人还怪好的嘞,直接将Remark42提供的代码复制粘贴进去:
<div id=remark42></div>
<script>
var remark_config = {
host: 'https://remark42.cloudworks.space',
site_id: 'solitary-dream.vercel.app',
theme: 'dark',
simple_view: false,
no_footer: true
}
</script>
<script>
!function(e, n) {
for (var o = 0; o < e.length; o++) {
var r = n.createElement("script")
, c = ".js"
, d = n.head || n.body;
"noModule"in r ? (r.type = "module",
c = ".mjs") : r.async = !0,
r.defer = !0,
r.src = remark_config.host + "/web/" + e[o] + c,
d.appendChild(r)
}
}(remark_config.components || ["embed"], document);
</script>
That's it,现在这个博客所有页面的下方都有Remark42的评论区了。
其他有趣的事情
- 像是这种Remark42部署在A域名,博客在B域名的情况,目前尚不能支持基于OAuth的认证,例如Github。作者说后续会修复,到时候再更新吧。我对OAuth了解太少了,对这个问题认知还是比较模糊。
- Remark42里有些的请求被写成了绝对路径,所以如果试图把它挂在非/下会遇到一些问题。
- AdBlock有些情况下会导致net::ERR_BLOCKED_BY_CLIENT。
- SameSite=None在Chrome无痕模式下不生效。
好吧,这些实际上并不是那么有趣,至少对于凌晨2点的我来说不是。
不过不管怎么说,好算是在周六一天内完成了这件事。原本计划周日做另一件事的,但被突如其来的加班通知打断了,那就先这样吧。