为什么选择 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 报错 undefined | footer 是必填字段,不能省略 | 使用 Component.Footer() |