为 Chirpy 添加 Twikoo 评论:一次插件开发记录
Chirpy 是个出色的主题,但它原生不支持 Twikoo 评论。我想在现有的 Giscus 系统旁添加 Twikoo,又不想修改主题源码。解决方案经过多次迭代:一开始使用 Jekyll 插件,后来尝试 footer.html 模板注入,最终采用本地构建后推送的工作流程,以确保所有部署平台的行为完全一致。
为什么要在 Giscus 之外再加一套评论系统?Giscus 本身很好——免费、无广告、评论数据存储在 GitHub Discussions 里,跟项目代码在同一个屋檐下。但它有一个门槛:读者必须拥有 GitHub 账号才能发言。这听起来不算什么,直到你真的遇到那些场景。一位来自中国的读者也许能稳定访问你的博客,却怎么也打不开 GitHub 的认证页面;另一位读者可能只是想说一句"好文章",却要为此注册一个他并不需要的账号。这种摩擦会直接损失评论——很多人看到登录页面就关掉了标签页。
Twikoo 提供了另一种路。它支持匿名评论,不需要任何第三方账号,读者打开页面就能打字。代价是你需要自己维护一个后端,可以是腾讯云 CloudBase、Vercel,或者自建服务器。对我来说,这个代价完全值得:同时提供 Giscus 和 Twikoo,就等于同时照顾了两类读者——那些愿意用 GitHub 身份发声的,和那些只想留下只言片语的。
但 Chirpy 主题的模板打包在 Ruby gem 里,你不能直接在项目目录中修改它们。官方只内置了 Giscus、Disqus 和 Utterances 三种评论系统。如果你想加 Twikoo,绕过 gem 的束缚,就得动主题的源文件。最直接的办法是 fork 主题,改完再用自己的 fork。但这引入了长期的维护成本:每次 Chirpy 发布更新,你都得把变动合并到自己的 fork 里,而评论功能本身并不会因为主题升级而有什么变化。我希望的方案是能跟上游主题保持同步的,不需要维护一个独立的 fork。
于是我开始寻找一种不触碰主题核心文件,就能把 Twikoo 注入到每篇文章中的方法。这个过程经历了三次尝试。
第一次尝试是写一个 Jekyll 插件。我创建了一个后渲染钩子(post-render hook),在 Jekyll 构建完每个文档之后运行。它检查输出是不是 HTML,验证当前文档是不是一篇文章,确认无误后,把 Twikoo 所需的容器和脚本注入到结束标签之前。这个方案在本地跑得很好,在 Cloudflare Pages 上也正常。但推上 GitHub Pages 后,评论模块消失了。问题出在 GitHub Pages 的构建环境对自定义 Jekyll 插件的处理方式上——它在构建过程中根本不会执行第三方插件。我开始以为是自己钩子类型选错了,于是换了钩子类型,加了错误日志,清了缓存,反复调整配置。折腾了几周之后才确认,这不是代码的问题,是平台本身不支持。完整的插件源码现在还在我的仓库里,路径是 _plugins/twikoo-inject.rb,供那些使用兼容平台的读者参考。
第二次尝试换了个思路,绕开插件系统。我用 Liquid 模板把 Twikoo 直接嵌入到 _includes/footer.html 里。footer.html 是站点模板的一部分,Jekyll 在正常构建周期中会处理它,不管插件有没有被加载。这个想法本身是成立的,但实际效果依然不稳定。GitHub Actions 的构建环境在处理模板 include 的细节上与其他平台存在差异,导致 footer include 在某些构建中触发了,在某些构建中没有。这时候我意识到,真正的问题不是用什么方式注入 Twikoo,而是不同的构建环境对同一份源码的解释并不一致。
第三次尝试彻底改变了思路。既然构建环境不可控,那就不让它们构建。我在本地先跑 bundle exec jekyll build,把生成的 _site 文件夹推送到部署仓库。托管平台只负责提供静态文件,不执行任何构建命令。这种方式下,不管你把 _site 部署到 GitHub Pages、Cloudflare Pages 还是 Netlify,输出都是你在本地看到的那一份。它不是用哪一个平台的标准去构建,而是你自己决定了标准的版本。
具体的注入方式仍然是利用了 _includes/footer.html 模板。区别在于,之前是让平台去渲染这个模板,现在是在本地渲染好再推送。为了防止重复注入,我在 Twikoo 容器上加了独特的数据属性作为标记,这样即使文章本身包含关于 Twikoo 的代码示例,注入逻辑也不会误判。这是一个自己写插件时很容易踩的坑——你的文章内容恰好提到了注入目标,结果把自己排除在外了。
UI 上我做了一个切换按钮。因为 Giscus 是由 Chirpy 主题自身渲染的,没办法隐藏,所以 Twikoo 默认不显示。读者点击"展开 Twikoo 评论"之后才会出现输入框,按钮文字会同步切换成"收起"。这样做的另一个好处是实现了懒加载:Twikoo 的 JavaScript 库只在第一次点击按钮时才加载并初始化。如果读者没有评论的意图,他们的浏览器就不会多下载一个库。
样式方面我用了 Chirpy 定义的 CSS 变量——--border-color、--text-muted、--link-color 这几个。切换按钮和分隔线在浅色和深色模式下自动适配,不需要为每种模式写两套样式。宽度对齐到 max-width: 800px,与文章正文保持一致。边距设成 0.5rem auto 2rem auto——顶部给一点呼吸空间,底部留出更充分的分隔,因为下面是 footer 的开始位置。
配置很简单。如果你只想用 Twikoo,不要在 theme 的评论 provider 字段里填任何值,否则 Chirpy 会因为不认识的 provider 报错。如果你想同时使用 Giscus 和 Twikoo(像我的博客这样),就把 provider 保留为 giscus,主题会正常渲染 Giscus,Twikoo 则通过我们注入的方式独立显示。另外需要在 _config.yml 里加上一行:
plugins_dir: _plugins
这行配置告诉 Jekyll 从 _plugins 目录加载自定义插件。如果缺少这行,Jekyll 可能找不到你放在那里的脚本,具体表现因调用方式而异。
部署方面,我现在的流程是这样的。源码和 _site 在同一个仓库里。本地构建完成后,通过 GitHub Actions 把 _site 目录推送到 GitHub Pages。对于 Cloudflare Pages 和 Netlify,同样推送 _site 目录,但在它们的控制台中关闭构建功能,只保留部署和托管静态文件的能力。Netlify 的免费版每月有 300 分钟的构建时间限制,跳过构建步骤还能省下这些额度给其他项目用。
如果你想自己配置一遍,各平台的设置要点是:GitHub 仓库设置中启用 Pages,选择 GitHub Actions 作为构建源;Cloudflare 创建 Pages 项目时关闭构建选项,部署目录设为 _site;Netlify 创建站点时同样关闭构建命令,发布目录设为 _site。然后写一个简短的部署脚本来自动化整个过程,每次需要发布时在本地先 bundle exec jekyll build,然后提交 _site 的变更并推送。你可以参考这个骨架:
#!/bin/bash
set -e
bundle exec jekyll build
cd _site
git add .
git commit -m "Deploy $(date)"
git push deploy main
本地构建工作流程有几个实实在在的好处。一致性是最重要的一条——你本地是什么样,线上就是什么样,因为用的是同一台机器、同一套 Ruby 版本和 gem 版本构建出来的。调试也变得更简单:如果线上页面出了问题,你把本地 _site 里对应的 HTML 打开,和线上的比较,差异一目了然。部署速度更快,推送 git 提交只需要几秒钟,不需要等平台慢慢跑构建。最后,你掌握了控制权——平台可能会在某个星期二悄悄升级 Ruby 版本,但你的构建环境只会在你主动升级时才会变化。
现在每篇文章底部都有两个评论系统。Giscus 默认可见,与主题风格融为一体。Twikoo 默认隐藏,通过切换按钮访问。它们各自独立工作,读者按自己的情况选。如果你只是在浏览内容,两个都可以忽略,页面不会因为你没评论就加载多余的脚本。
完整的源代码在我的 GitHub 仓库里。你需要关注三个文件:_includes/footer.html(模板注入的逻辑)、_plugins/twikoo-inject.rb(插件注入版本,供兼容平台使用)、以及 _config.yml 中与 Twikoo 相关的配置项。这些文件可以直接复制到你的项目中使用。
最后记录一下这个方案在迭代过程中的关键节点:4 月 25 日完成初始版本,包含切换 UI 和懒加载;同一天将钩子从 site:post_render 改为 documents:post_render,提升了兼容性;5 月 2 日修复了自我排除的 bug——使用独特 data 属性做标记后,文章内容不再影响注入结果;5 月 7 日尝试 footer.html 模板注入作为纯插件方案的替代;5 月 8 日最终转向本地构建工作流程。到这一步,我在不同平台上看到的终于不再是三种不同的结果。