个人
搭建
深浅模式
更新: 8/13/2026 字数: 0 字 时长: 0 分钟
静态博客最尴尬的一点,是评论这件事。页面能静态生成,留言却得有个地方存。以前折腾 WordPress / 自建后端时评论几乎是顺带的能力;迁到 VitePress 之后,反而要想清楚:后端放哪、前端怎么挂、本地开发怎么跨域、暗色主题怎么不崩。
这次选的是 Twikoo-Cloudflare,后端用 Cloudflare Workers + D1,前端接到主题的 doc-after 插槽。
doc-after
具体流程请按照官方文档为准,上面这里超链接指向了对应的github地址,可以自行前往,并查看readme的安装步骤,本篇文章主要是讲解我在安装过程中遇到了哪些问题。
下面按「怎么搭 → 前端怎么接 → 踩了什么坑」写一遍,方便以后自己翻,也方便同样用 VitePress 的人少走弯路。
评论方案很多,Giscus、Waline、Twikoo 都常见。我这边更在意这几件事:
*.workers.dev
Twikoo 的 Cloudflare 方案用 Workers 跑云函数,D1 存评论,R2 可以顺带做图片存储。冷启动通常比「Serverless + 外部 Mongo」舒服一些。部署完成后,直接访问 Worker 地址,正常会返回类似:
{ "code": 100, "message": "Twikoo 云函数运行正常,请参考 https://twikoo.js.org/frontend.html 完成前端的配置", "version": "1.6.44" }
看到这个,后端就算活了。
Worker 默认地址类似:
https://twikoo-cloudflare.xxx.workers.dev
生产上肯定要换成自己的域名。这里有个很容易忽略的点:Twikoo Cloudflare 部署里,R2_PUBLIC_URL 经常会先占一个子域,比如本站的:
R2_PUBLIC_URL
https://twikoo.ylmty.cc → R2 公共访问 https://comment.ylmty.cc → Worker 评论接口
如果把 Worker 也绑到已经给 R2 用的 twikoo.ylmty.cc,DNS / 自定义域会打架。所以评论 API 我单独用了 comment.ylmty.cc。
twikoo.ylmty.cc
comment.ylmty.cc
绑定方式可以用 Wrangler 的 triggers:
name = "twikoo-cloudflare" workers_dev = true [[routes]] pattern = "comment.ylmty.cc" custom_domain = true
然后执行:
npx wrangler triggers deploy --config wrangler.toml
前端 envId 就写成带 https:// 的自定义域名:
envId
https://
const TWIKOO_PROD_ENV_ID = "https://comment.ylmty.cc";
主题里没有单独的 Layout.vue,而是在 theme/index.ts 里用 h(DefaultTheme.Layout)。所以评论组件挂在默认主题的 doc-after 插槽:
Layout.vue
theme/index.ts
h(DefaultTheme.Layout)
import Twikoo from "./components/Twikoo.vue"; export default { extends: DefaultTheme, Layout() { const props: Record<string, any> = {}; const { frontmatter } = useData(); props.class = frontmatter.value?.layout || ""; return h(DefaultTheme.Layout, props, { "doc-after": () => h(Twikoo), }); }, };
核心组件大致是这样:
<template> <div v-if="showComment" class="lmt-twikoo"> <div id="twikoo"></div> </div> </template> <script setup lang="ts"> import { computed, nextTick, onMounted, watch } from "vue"; import { useData, useRoute } from "vitepress"; const TWIKOO_PROD_ENV_ID = "https://comment.ylmty.cc"; function getTwikooEnvId() { if (import.meta.env.DEV && typeof window !== "undefined") { return `${window.location.origin}/twikoo-api`; } return TWIKOO_PROD_ENV_ID; } const route = useRoute(); const { frontmatter, page } = useData(); const showComment = computed(() => { if (frontmatter.value.comment === false) return false; if (frontmatter.value.comment === true) return true; const path = (page.value.relativePath || "").replace(/\\/g, "/"); if (path === "pages/friend.md" || frontmatter.value.layout === "lmt-friend") { return true; } if ( path.startsWith("articles/") && path.endsWith(".md") && !path.endsWith("/index.md") && path !== "articles/index.md" ) { return true; } return false; }); const initTwikoo = async () => { if (typeof window === "undefined" || !showComment.value) return; await nextTick(); const el = document.getElementById("twikoo"); if (!el) return; el.innerHTML = ""; const twikooMod: any = await import("twikoo"); const init = typeof twikooMod.default === "function" ? twikooMod.default : twikooMod.init || twikooMod.default?.init; if (typeof init !== "function") return; await init({ envId: getTwikooEnvId(), el: "#twikoo", path: route.path, lang: "zh-CN", }); }; watch(() => route.path, () => initTwikoo()); onMounted(() => initTwikoo()); </script>
几个设计点:
window
navigator
path: route.path
单页也可以用 frontmatter 覆盖:
--- comment: false ---
或强制打开:
--- comment: true ---
友链页是 layout: lmt-friend。VitePress 会把整页换成注册好的自定义组件,默认文档布局的 doc-after 根本不会渲染。
layout: lmt-friend
所以只把 Twikoo 塞进 Layout 插槽不够,友链页要在 Friends.vue 里再挂一次:
Friends.vue
<!-- 友链为自定义 layout,doc-after 不生效,需在此挂载评论 --> <Twikoo />
这是静态站主题里很常见的一类坑:插槽只对默认 Doc 布局生效,自定义页要自己接。
twikoo.init is not a function
文档和很多示例会写成:
const twikoo = await import("twikoo"); twikoo.init({ ... });
但 twikoo 这个 npm 包的 UMD 构建,默认导出本身就是 init 函数,不是 { init } 对象。于是你会在控制台看到:
twikoo
{ init }
TypeError: twikoo.init is not a function
稳妥写法:
const twikooMod: any = await import("twikoo"); const init = typeof twikooMod.default === "function" ? twikooMod.default : twikooMod.init || twikooMod.default?.init; await init({ envId, el: "#twikoo", path: route.path, lang: "zh-CN" });
另外它没有官方类型包,需要自己补一个声明,否则 TypeScript 会报「无法找到模块声明文件」:
declare module "twikoo" { interface TwikooInitOptions { envId: string; el: string; path?: string; lang?: string; } function init(options: TwikooInitOptions): Promise<void> | void; export default init; export { init }; }
生产域名访问评论接口通常没问题。本地如果用:
http://192.168.x.x:5173
去打 https://comment.ylmty.cc,浏览器很容易直接 CORS 拦掉。localhost 在 Twikoo 里一般有特殊放行,但局域网 IP 不一定。
https://comment.ylmty.cc
localhost
更省事的做法是开发环境走 Vite 同源代理:
// docs/.vitepress/config.mts server: { host: "0.0.0.0", port: 5173, proxy: { "/twikoo-api": { target: "https://comment.ylmty.cc", changeOrigin: true, rewrite: (path) => path.replace(/^\/twikoo-api/, "") || "/", }, }, },
前端开发态把 envId 指到当前源:
function getTwikooEnvId() { if (import.meta.env.DEV && typeof window !== "undefined") { return `${window.location.origin}/twikoo-api`; } return "https://comment.ylmty.cc"; }
改代理后记得重启 pnpm docs:dev,否则不生效。
pnpm docs:dev
管理面板里也可以配 CORS_ALLOW_ORIGIN。开发阶段可以先留空;上线后再收紧成正式域名列表。
CORS_ALLOW_ORIGIN
Twikoo 内嵌的是偏 Element UI 风格的输入结构,昵称 / 邮箱 / 网址的边框实际画在 .el-input__inner 上,不是 Element Plus 那套 .el-input__wrapper。
.el-input__inner
.el-input__wrapper
如果为了去白底,写成:
.el-input__inner { border: none !important; background: transparent !important; }
暗色背景下就会出现:标签还在,输入框轮廓没了,看起来像字漂在页面上。
更稳的是显式给边框和背景:
.lmt-twikoo .twikoo { .el-input__inner { color: var(--vp-c-text-1) !important; background-color: var(--lmt-card-bg) !important; border: 1px solid var(--vp-c-divider) !important; border-radius: var(--lmt-radius-md) !important; } .el-textarea__inner { background-color: var(--lmt-card-bg) !important; border: 1px solid var(--vp-c-divider) !important; border-radius: var(--lmt-radius-lg) !important; } }
管理面板同理,默认黄底提示条、小灰按钮、扁平列表,和站点气质差一截。后面单独给 .tk-admin 做了卡片化、圆角、品牌色提示条和分页样式,观感会好很多。
.tk-admin
这是最容易误会成「配置错了」的问题。
输入框里填 数字@qq.com 时,前端预览会直接拼 QQ 头像地址,所以你能看到自己的 QQ 头像。但评论真正发出后,服务端会走另一套逻辑:先尝试官方 QQ 头像接口;而 Twikoo 源码里已经写明这个接口失效了。失败后就回落到 Gravatar / Cravatar。
数字@qq.com
于是就出现:
把 GRAVATAR_CDN 改成 cravatar.cn 只是换了 Gravatar 镜像,不会自动同步 QQ 头像。要两边一致,可以:
GRAVATAR_CDN
cravatar.cn
q1.qlogo.cn
第一种改配置就能做;第二种更彻底,但要动 Cloudflare 上的 Twikoo Worker。
落地上大概是这几块:
Cloudflare ├─ Worker: twikoo-cloudflare │ └─ 自定义域: comment.ylmty.cc ├─ D1: twikoo(评论 / 配置) └─ R2: twikoo(图片,公共域 twikoo.ylmty.cc) VitePress 主题 ├─ theme/index.ts # doc-after 挂载 ├─ components/Twikoo.vue # 初始化 / 白名单 / 代理 envId ├─ pages/Friends.vue # 自定义布局补挂评论 ├─ style/twikoo.css # 评论区 + 管理面板样式 └─ type/twikoo.d.ts # 模块声明
整体不算复杂,但真要「本地好调、线上好看、路径不串、暗色不翻车」,细节比想象中多。静态博客接评论,难的往往不是选哪个产品,而是把主题插槽、SPA 路由、跨域和第三方包的导出方式这些边角料一次收拾干净。
VitePress 接入 Twikoo:Cloudflare 部署、前端集成与踩坑记录
更新: 8/13/2026 字数: 0 字 时长: 0 分钟
静态博客最尴尬的一点,是评论这件事。页面能静态生成,留言却得有个地方存。以前折腾 WordPress / 自建后端时评论几乎是顺带的能力;迁到 VitePress 之后,反而要想清楚:后端放哪、前端怎么挂、本地开发怎么跨域、暗色主题怎么不崩。
这次选的是 Twikoo-Cloudflare,后端用 Cloudflare Workers + D1,前端接到主题的
doc-after插槽。具体流程请按照官方文档为准,上面这里超链接指向了对应的github地址,可以自行前往,并查看readme的安装步骤,本篇文章主要是讲解我在安装过程中遇到了哪些问题。
下面按「怎么搭 → 前端怎么接 → 踩了什么坑」写一遍,方便以后自己翻,也方便同样用 VitePress 的人少走弯路。
为什么是 Twikoo + Cloudflare
评论方案很多,Giscus、Waline、Twikoo 都常见。我这边更在意这几件事:
*.workers.devTwikoo 的 Cloudflare 方案用 Workers 跑云函数,D1 存评论,R2 可以顺带做图片存储。冷启动通常比「Serverless + 外部 Mongo」舒服一些。部署完成后,直接访问 Worker 地址,正常会返回类似:
2
3
4
5
看到这个,后端就算活了。
自定义域名:别和 R2 抢同一个子域
Worker 默认地址类似:
生产上肯定要换成自己的域名。这里有个很容易忽略的点:Twikoo Cloudflare 部署里,
R2_PUBLIC_URL经常会先占一个子域,比如本站的:2
如果把 Worker 也绑到已经给 R2 用的
twikoo.ylmty.cc,DNS / 自定义域会打架。所以评论 API 我单独用了comment.ylmty.cc。绑定方式可以用 Wrangler 的 triggers:
2
3
4
5
6
然后执行:
前端
envId就写成带https://的自定义域名:前端接入:组件 + Layout 插槽
主题里没有单独的
Layout.vue,而是在theme/index.ts里用h(DefaultTheme.Layout)。所以评论组件挂在默认主题的doc-after插槽:2
3
4
5
6
7
8
9
10
11
12
13
核心组件大致是这样:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
几个设计点:
window/navigatorpath: route.path:评论按文章路径隔离单页也可以用 frontmatter 覆盖:
2
3
或强制打开:
2
3
坑 1:自定义 Layout 吃不到
doc-after友链页是
layout: lmt-friend。VitePress 会把整页换成注册好的自定义组件,默认文档布局的doc-after根本不会渲染。所以只把 Twikoo 塞进 Layout 插槽不够,友链页要在
Friends.vue里再挂一次:2
这是静态站主题里很常见的一类坑:插槽只对默认 Doc 布局生效,自定义页要自己接。
坑 2:
twikoo.init is not a function文档和很多示例会写成:
2
但
twikoo这个 npm 包的 UMD 构建,默认导出本身就是 init 函数,不是{ init }对象。于是你会在控制台看到:稳妥写法:
2
3
4
5
6
7
另外它没有官方类型包,需要自己补一个声明,否则 TypeScript 会报「无法找到模块声明文件」:
2
3
4
5
6
7
8
9
10
11
12
坑 3:本地局域网 IP 的 CORS
生产域名访问评论接口通常没问题。本地如果用:
去打
https://comment.ylmty.cc,浏览器很容易直接 CORS 拦掉。localhost在 Twikoo 里一般有特殊放行,但局域网 IP 不一定。更省事的做法是开发环境走 Vite 同源代理:
2
3
4
5
6
7
8
9
10
11
12
前端开发态把
envId指到当前源:2
3
4
5
6
改代理后记得重启
pnpm docs:dev,否则不生效。管理面板里也可以配
CORS_ALLOW_ORIGIN。开发阶段可以先留空;上线后再收紧成正式域名列表。坑 4:暗色主题下输入框「看不见边框」
Twikoo 内嵌的是偏 Element UI 风格的输入结构,昵称 / 邮箱 / 网址的边框实际画在
.el-input__inner上,不是 Element Plus 那套.el-input__wrapper。如果为了去白底,写成:
2
3
4
暗色背景下就会出现:标签还在,输入框轮廓没了,看起来像字漂在页面上。
更稳的是显式给边框和背景:
2
3
4
5
6
7
8
9
10
11
12
13
14
管理面板同理,默认黄底提示条、小灰按钮、扁平列表,和站点气质差一截。后面单独给
.tk-admin做了卡片化、圆角、品牌色提示条和分页样式,观感会好很多。坑 5:QQ 邮箱预览头像和发出后不一致
这是最容易误会成「配置错了」的问题。
输入框里填
数字@qq.com时,前端预览会直接拼 QQ 头像地址,所以你能看到自己的 QQ 头像。但评论真正发出后,服务端会走另一套逻辑:先尝试官方 QQ 头像接口;而 Twikoo 源码里已经写明这个接口失效了。失败后就回落到 Gravatar / Cravatar。于是就出现:
把
GRAVATAR_CDN改成cravatar.cn只是换了 Gravatar 镜像,不会自动同步 QQ 头像。要两边一致,可以:q1.qlogo.cn地址,不再依赖已失效接口第一种改配置就能做;第二种更彻底,但要动 Cloudflare 上的 Twikoo Worker。
最终结构小结
落地上大概是这几块:
2
3
4
5
6
7
8
9
10
11
12
整体不算复杂,但真要「本地好调、线上好看、路径不串、暗色不翻车」,细节比想象中多。静态博客接评论,难的往往不是选哪个产品,而是把主题插槽、SPA 路由、跨域和第三方包的导出方式这些边角料一次收拾干净。