前言

之前我们创建博客后已经在 hugo.toml 配置文件中开启了文章评论功能

toml
[params]
  comments = true

但这只是打开了评论开关,并没有真正实现评论功能,因为 hugo 搭建的是竞态网站,并不提供数据库存储服务,因此需要我们自己实现一套评论系统。

好消息是目前已经有成熟稳定的方案,我们只需要简单几步和少量代码将其接入到自己的博客即可使用。这里我们介绍的是基于 github Discussion 的 Giscus App

一、评论系统:Giscus(完整生产方案)

选择 Giscus 的理由:基于 GitHub Discussions,免费稳定、无广告、无需数据库、支持 Markdown/代码高亮/表情反应,且与 GitHub 账号体系打通(对技术博客读者非常自然)。

步骤 1:GitHub 端准备

  1. 准备仓库:新建(或沿用现有)一个公开 GitHub 仓库,用于存放评论数据。仓库必须是 public,否则 Giscus 无法读取。

  2. 开启 Discussions:进入仓库 → SettingsGeneralFeatures → 勾选 Discussions

  3. 安装 Giscus App:访问 github.com/apps/giscus → 点击 Install → 选择 Only select repositories → 勾选你的仓库 → 保存。遵循最小权限原则,不要授权给所有仓库。

  4. 获取配置参数:访问 giscus.app(有中文界面),填写你的仓库名,选择:

    • 页面 ↔️ Discussion 映射pathname(最稳定,换域名不丢评论)
    • Discussion 分类Announcements(该分类下的讨论不会出现在仓库首页动态,更干净)
    • 主题:先随便选一个,后面 Hugo 端会配置自动切换
    • 语言zh-CN

    页面底部会生成一段 <script>记录下其中的 data-repodata-repo-iddata-categorydata-category-id 四个值。

步骤 2:安全加固(防止脚本被盗用)

在 Hugo 站点仓库的根目录(与 hugo.toml 同级,不是 static/ 目录)创建 giscus.json

json
{
  "origins": ["https://example.org"],
  "defaultCommentOrder": "newest"
}

这会让 Giscus 只在你自己的域名下加载,防止他人盗用你的评论组件。

步骤 3:Hugo 配置

hugo.toml[params] 块内新增

toml
[params.giscus]
repo = "yourname/yourrepo"           # 例:dake/blog-comments
repoId = "R_xxxxxxxxxx"              # 从 giscus.app 复制
category = "Announcements"
categoryId = "DIC_xxxxxxxxxx"        # 从 giscus.app 复制
mapping = "pathname"
strict = "0"                         # 0=宽松匹配(推荐),1=严格标题匹配
reactionsEnabled = "1"               # 启用表情反应
emitMetadata = "0"                     # 不发送元数据
inputPosition = "bottom"             # 评论框在底部
lightTheme = "light"
darkTheme = "dark"
lang = "zh-CN"

注意:repoIdcategoryId 必须以字符串形式保留(带引号),因为它们是字母数字混合 ID。

步骤 4:创建评论模板

创建文件 layouts/partials/comments.html,内容如下(完整代码,含 PaperMod 暗黑/亮色主题自动同步):

html
{{- if and .Site.Params.giscus .Site.Params.giscus.repo -}}
<div class="comments-wrapper" style="margin-top: 3rem;">
    <div class="comments-header" style="text-align: center; margin-bottom: 1.5rem;">
        <h3 style="font-size: 1.25em; font-weight: 700; margin-bottom: 0.25rem;">评论</h3>
        <p style="font-size: 0.9rem; color: var(--secondary);">使用 GitHub 账号登录后参与讨论</p>
    </div>
    <div id="giscus-container"></div>
</div>

<script>
    const getStoredTheme = () => {
        const pref = localStorage.getItem("pref-theme");
        if (pref === "dark") return "{{ .Site.Params.giscus.darkTheme | default "dark" }}";
        if (pref === "light") return "{{ .Site.Params.giscus.lightTheme | default "light" }}";
        // 用户从未手动切换过,跟随系统偏好
        return "preferred_color_scheme";
    };

    const setGiscusTheme = () => {
        const sendMessage = (message) => {
            const iframe = document.querySelector('iframe.giscus-frame');
            if (iframe) {
                iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app');
            }
        };
        sendMessage({ setConfig: { theme: getStoredTheme() } });
    };

    document.addEventListener("DOMContentLoaded", () => {
        // 创建 giscus 挂载容器
        const giscusDiv = document.createElement("div");
        giscusDiv.className = "giscus";
        document.querySelector("#giscus-container").appendChild(giscusDiv);

        // 动态创建 script,确保 theme 初始值正确
        const giscusAttributes = {
            "src": "https://giscus.app/client.js",
            "data-repo": "{{ .Site.Params.giscus.repo }}",
            "data-repo-id": "{{ .Site.Params.giscus.repoId }}",
            "data-category": "{{ .Site.Params.giscus.category }}",
            "data-category-id": "{{ .Site.Params.giscus.categoryId }}",
            "data-mapping": "{{ .Site.Params.giscus.mapping | default "pathname" }}",
            "data-strict": "{{ .Site.Params.giscus.strict | default "0" }}",
            "data-reactions-enabled": "{{ .Site.Params.giscus.reactionsEnabled | default "1" }}",
            "data-emit-metadata": "{{ .Site.Params.giscus.emitMetadata | default "0" }}",
            "data-input-position": "{{ .Site.Params.giscus.inputPosition | default "bottom" }}",
            "data-theme": getStoredTheme(),
            "data-lang": "{{ .Site.Params.giscus.lang | default "zh-CN" }}",
            "data-loading": "lazy",
            "crossorigin": "anonymous",
            "async": "",
        };

        const giscusScript = document.createElement("script");
        Object.entries(giscusAttributes).forEach(([key, value]) => {
            giscusScript.setAttribute(key, value);
        });
        document.querySelector("#giscus-container").appendChild(giscusScript);

        // 监听 PaperMod 主题切换按钮(桌面端 + 移动端浮动按钮)
        const themeSwitcher = document.querySelector("#theme-toggle");
        if (themeSwitcher) {
            themeSwitcher.addEventListener("click", setGiscusTheme);
        }
        const themeFloatSwitcher = document.querySelector("#theme-toggle-float");
        if (themeFloatSwitcher) {
            themeFloatSwitcher.addEventListener("click", setGiscusTheme);
        }
    });
</script>
{{- else -}}
{{- warnf "comments.html: Giscus 参数未配置完整,跳过渲染" -}}
{{- end -}}

步骤 5:可选 CSS 美化

如果你希望评论区上方的标题更美观,创建 assets/css/extended/comments.css

css
.comments-wrapper {
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}

.comments-header h3 {
    color: var(--primary);
}

.comments-header p {
    color: var(--secondary);
    margin: 0;
}

assets/css/extended/ 是 PaperMod 约定自动加载自定义 CSS 的目录,无需额外配置。

步骤 6:验证

  1. 本地重新运行 hugo server -D -F --disableFastRender,打开任意文章页,滚动到底部。
  2. 如果看到"使用 GitHub 账号登录后参与讨论"和 Giscus 加载框,说明成功。
  3. 点击 PaperMod 顶部的 🌙/☀️ 主题切换按钮,Giscus 评论区应同步切换暗黑/亮色。