为什么选择 Twikoo

Quartz 默认不带评论功能。常见的评论方案中,Giscus 依赖 GitHub 登录,对普通访客不友好。Twikoo 支持匿名评论、无需登录,部署简单,适合个人笔记站。

前提条件

  • 已部署 Twikoo 后端(Docker / Vercel / 云函数均可)
  • Twikoo 后端地址可通过 HTTPS 访问(避免混合内容错误)
  • Quartz v4 项目已正常运行

如果站点是 HTTPS 但 Twikoo 后端是 HTTP,需要通过反向代理将 Twikoo 挂到同域名下,例如 https://yourdomain.com/twikoo/,具体配置参考 Nginx Proxy Manager 的 Advanced 选项。


一、创建 Twikoo 组件

创建 .quartz/TwikooComments.tsx

import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
 
interface Options {
  envId: string
}
 
export default ((opts: Options) => {
  const TwikooComments: QuartzComponent = ({
    displayClass,
    fileData,
  }: QuartzComponentProps) => {
    // frontmatter 中设置 comments: false 可禁用评论
    if (fileData.frontmatter?.comments === false) {
      return <></>
    }
 
    return (
      <div
        class={classNames(displayClass, "twikoo-comments")}
        id="twikoo-container"
        data-env-id={opts.envId}
      />
    )
  }
 
  TwikooComments.afterDOMLoaded = `
document.addEventListener("nav", () => {
  const container = document.getElementById("twikoo-container")
  if (!container) return
 
  const envId = container.dataset.envId
 
  // 确保 Twikoo CSS 在 <head> 中,data-persist 防止 SPA 导航时被移除
  if (!document.head.querySelector("link[data-twikoo-css]")) {
    const link = document.createElement("link")
    link.rel = "stylesheet"
    link.href = "https://cdn.jsdelivr.net/npm/twikoo@1.6.39/dist/twikoo.css"
    link.setAttribute("data-persist", "")
    link.setAttribute("data-twikoo-css", "")
    document.head.appendChild(link)
  }
 
  // 清空容器,重新初始化
  container.innerHTML = ""
 
  function initTwikoo() {
    twikoo.init({
      envId: envId,
      el: "#twikoo-container",
    })
  }
 
  if (typeof twikoo !== "undefined") {
    initTwikoo()
  } else {
    const script = document.createElement("script")
    script.src = "https://cdn.jsdelivr.net/npm/twikoo@1.6.39/dist/twikoo.all.min.js"
    script.onload = initTwikoo
    document.head.appendChild(script)
  }
})`
 
  return TwikooComments
}) satisfies QuartzComponentConstructor<Options>

关键设计说明

SPA 兼容:Quartz 的 SPA 模式在页面切换时会移除 <head> 中所有没有 data-persist 属性的元素。Twikoo 的 CSS <link> 必须带上 data-persist,否则导航后样式丢失。

按需加载twikoo.all.min.js 只在首次需要时加载,后续页面切换复用已加载的全局对象。

禁用评论:在任意笔记的 frontmatter 中添加 comments: false 即可隐藏该页评论。


二、注册组件到布局

创建 .quartz/quartz.layout.ts,在 afterBody 中添加 Twikoo 组件:

import TwikooComments from "./quartz/components/TwikooComments"
 
export const sharedPageComponents: SharedLayout = {
  head: Component.Head(),
  header: [],
  afterBody: [
    TwikooComments({
      envId: "https://yourdomain.com/twikoo/",  // 替换为你的 Twikoo 后端地址
    }),
  ],
  footer: Component.Footer(),
}

envId 是 Twikoo 后端的访问地址。


三、修复全局样式冲突

Quartz 的全局 CSS 对 img 设置了 margin: 1rem 0,会导致 Twikoo 头像偏移。在 .quartz/custom.scss 中覆盖:

@use "./base.scss";
 
// Twikoo 评论组件样式覆盖
.tk-avatar img {
    margin: 0 !important;
}

四、更新 CI/CD 流水线

.cnb.yml 的构建阶段添加文件复制:

# 复制 .quartz 目录中的自定义配置
cp /workspace/.quartz/quartz.config.ts .
cp /workspace/.quartz/quartz.layout.ts .
cp /workspace/.quartz/custom.scss quartz/styles/custom.scss
cp /workspace/.quartz/TwikooComments.tsx quartz/components/TwikooComments.tsx

五、文件结构

仓库根目录/
├── .quartz/
│   ├── quartz.config.ts          ← 站点配置
│   ├── quartz.layout.ts          ← 布局配置(注册 Twikoo)
│   ├── custom.scss               ← 自定义样式(修复头像偏移)
│   └── TwikooComments.tsx        ← Twikoo 评论组件
├── .cnb.yml                      ← CI/CD 流水线
├── index.md
└── ...

六、踩坑记录

问题原因解决方案
SPA 导航后评论样式丢失Quartz SPA 路由器移除 <head> 中无 data-persist 的元素CSS <link> 添加 data-persist 属性
HTTPS 站点无法加载 Twikoo混合内容(Mixed Content)被浏览器拦截通过反向代理将 Twikoo 挂到同域名 HTTPS 路径下
头像位置偏移Quartz 全局 img { margin: 1rem 0 } 影响custom.scss 中用 .tk-avatar img { margin: 0 !important } 覆盖
部署后样式未更新Nginx Proxy Manager 缓存NPM Advanced 中添加 proxy_cache_bypass 1; proxy_no_cache 1;
Footer 报错 undefinedfooter 是必填字段,不能省略使用 Component.Footer()