Ghost Comments UI 开发与发布全指南:从评论区嵌入式组件到 jsDelivr 版本管理
Comments UI(npm 包名 @tryghost/comments-ui)是 Ghost 中内嵌在每篇博文底部的评论组件,随文章页面以 UMD 脚本形式从 CDN 加载。本指南以 apps/comments-ui/README.md 为骨架,系统讲解该公共前端应用(public app)的本地开发环境搭建、构建 / 测试命令体系,以及基于「自动 patch + 手动 minor/major」的双轨发布机制,并结合仓库源码说明它是如何被 Ghost 主题挂载、如何读取配置与做版本锁定的。
Comments UI 在 Ghost 生态中的角色
在动手开发前,先明确这个包的定位:它不是一个独立页面,而是被主题作者通过 {{comments}} Handlebars helper 注入到文章模板底部的一个「公共应用」。helper 的输出只有一行 <script>,核心逻辑见 ghost/core/core/frontend/helpers/comments.js,脚本地址与站点信息全部通过 data-* 属性传递给组件:
<script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>
其中 scriptUrl 由 getFrontendAppConfig('comments') 从 Ghost 服务端配置中解析(ghost/core/core/frontend/utils/frontend-apps.js),真正指向 CDN:
- 配置位置:
ghost/core/core/shared/config/defaults.json中的comments节点,当前为https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js,version为1.6(默认锁定 major.minor 一行,即允许 CDN 在该行内自动升级 patch,对应 README 中「同一 major/minor 行的站点自动获得 patch」的说法); - 配置读取时用
url.replace('{version}', appVersion)把版本号填入 URL 模板; - 组件侧则把
data-ghost-comments、data-api、data-key、data-post-id、data-color-scheme、data-avatar-saturation、data-accent-color、data-comments-enabled、data-count、data-title、data-publication、data-locale、data-admin等选项作为运行时输入,解析后的类型定义见 apps/comments-ui/src/app-context.ts 中的CommentsOptions。
因此,本包的产物是「一个浏览器可直接执行的 UMD bundle」,而不是 npm 库的 ESM 源码——这也决定了下面所有开发、构建与测试命令都围绕 UMD 产物展开。
前置条件与开发环境搭建
1. Monorepo 初始化
Comments UI 处于 Ghost pnpm workspace monorepo 内,首次开发前需在仓库根目录执行:
pnpm setup
该命令会完成 monorepo 的依赖安装、构建顺序等基础配置,之后才可启动任意 app。
2. 方式一:从仓库根目录通过 Ghost 驱动开发(推荐)
README 推荐的日常方式是让 Comments UI 运行在真实的 Ghost 环境中,在根目录执行:
pnpm dev:public
根目录 package.json 中该脚本被定义为 pnpm nx run ghost-monorepo:docker:dev:public,即通过 Nx + Docker 组合启动标准开发环境,同时开启各「public-app watcher」;Comments UI 的 watcher 定义在其 package.json 的 nx.targets.dev 中,实际执行的命令是 vite build --watch --mode development。也就是说 Comments UI 的改动会被即时重新打包,而运行中的 Ghost 直接引用新产物,实现接近热更新的开发体验。
若需要连同 Analytics、本地存储等附加服务一起启动,可使用增强版:
pnpm dev:full
3. 方式二:只开发该包本身
不启动 Ghost,在 apps/comments-ui 目录内单独运行:
pnpm build # 一次性构建
pnpm dev # 监听并增量重建 UMD bundle
pnpm test # 运行类型检查与单元测试
pnpm test:acceptance # 运行浏览器端验收测试
pnpm lint # lint 代码并检查类型
以下逐一拆解每个命令背后的行为与配置。
构建与开发模式:产物、React 17 锁定与去指纹化
UMD 构建流程
构建配置见 apps/comments-ui/vite.config.mts,它复用 monorepo 公共配置 publicAppViteConfig(来自 configs/vite-public-app),入口为 src/index.tsx:
- 生产产物被写入
umd/目录,这是 npmfiles白名单(umd/、LICENSE、README.md)与unpkg字段(umd/comments-ui.umd.js)指向的地方; prepublishOnly: pnpm build保证发布到 npm 前一定会重新产出最新的 UMD;preview服务端口为 7173,本地 vite dev server 端口为 5368。
一个值得注意的工程细节:包依赖固定使用 React 17(react: catalog:react17),而 monorepo 顶层提升了 React 18。为避免产物中出现两个 React 实例,Vite 配置做了 dedupe: ['react', 'react-dom', '@tryghost/debug'],并把 react / react-dom 通过 alias 强制指向本包 node_modules 中安装的 React 17;vitest 的 server.deps.inline 则把 @tiptap、@headlessui 等依赖同样内联,使它们的 React import 也命中同一 alias。
启动引导逻辑
入口 apps/comments-ui/src/index.tsx 完整演示了「UMD 自举」的过程:
- 通过
document.currentScript找到注入的<script data-ghost-comments>标签(ESM 开发模式下降级为querySelector); - 在脚本标签之前插入并复用容器
<div id="ghost-comments-root">。根节点 ID 定义于 apps/comments-ui/src/utils/constants.ts,注释特别说明:不能使用ghost-comments作为 ID,否则会破坏页面加载后注入 div 导致的#ghost-comments锚点滚动; - 从 URL hash 解析
initialCommentId,用于深链定位单条评论; - 清理 URL 上的
?token=参数(避免把一次性鉴权令牌留在地址栏); - 用
ReactDOM.render把<App/>挂载进容器,App 再读取 script 标签上的全部data-*选项。
评论区的首屏数据与会员鉴权在 apps/comments-ui/src/app.tsx 中编排:默认通过 IntersectionObserver(阈值 0.1)实现懒加载——只有当评论区进入视口才调用 /members/api/ 初始化与评论拉取;若存在 permalink(initialCommentId)则跳过懒加载立即初始化,并通过分页翻页、展开父评论回复等方式滚动定位到目标评论。此外当检测到管理员登录(角色限 Owner / Administrator / Super Editor,见 ALLOWED_MODERATORS)时,会通过隐藏的 AuthFrame 加载 Admin API 的完整评论数据,在前台渲染出审核/置顶等操作入口。
去指纹化构建插件
vite-plugin-strip-fingerprinting.ts 是一个面向产物的特殊插件:它会把 ProseMirror / tiptap 系列依赖中访问高熵指纹 API 的代码(navigator.vendor、navigator.platform、navigator.maxTouchPoints)替换为基于 navigator.userAgent 的等价判断。原因是该 UMD 通过 cdn.jsdelivr.net 分发,而被 DuckDuckGo Tracker Radar 及 Safari 26+ 的 Advanced Fingerprinting Protection 视为指纹风险域的脚本会受限。插件 enforce: 'pre',在 transform 阶段对 prosemirror-view、prosemirror-keymap、prosemirror-commands、w3c-keyname、@tiptap/core 的产物做字符串级替换,并在 buildEnd 时校验每个替换模式是否都被命中,若依赖升级导致模式失配会给出 warning,防止静默漏替换。
测试体系:单元测试与浏览器验收测试
类型与单元测试
pnpm test:types:分别以应用与 Node 两套 tsconfig 执行tsc --noEmit;pnpm test:unit:Vitest 运行test/unit/**/*.test.{js,jsx,ts,tsx}并生成覆盖率报告(vitest run --coverage);pnpm test=pnpm run '/^test:(types|unit)$/',即顺序执行上述两者。
单元测试的 setup 文件 apps/comments-ui/src/setup-tests.ts 引入 @testing-library/jest-dom 断言、每例之后 cleanup(),并为 jsdom 环境 mock 了 ResizeObserver。现有用例覆盖:Ghost API 封装(test/unit/utils/api.test.ts)、Admin API(test/unit/utils/admin-api.test.ts)、评论线程树构建(thread-graph.test.ts)、hooks 与头像渲染(hooks.test.tsx、components/content/avatar.test.tsx),以及专门验证去指纹替换后浏览器检测结果等价的 browser-detection-equivalence.test.ts。
浏览器验收测试
Playwright 配置见 apps/comments-ui/playwright.config.ts,关键参数如下:
| 项 | 值 | 说明 |
|---|---|---|
testDir |
./test/e2e |
验收测试目录 |
E2E_PORT |
7175 | 预览服务端口(dev:test 脚本:vite build && vite preview --port 7175) |
fullyParallel |
true |
文件级并行 |
retries |
CI 下 2 次 | 本地 0 次 |
timeout |
20000ms(slowmo 下 100000ms) | 可被 TIMEOUT 环境变量覆盖 |
| 默认浏览器 | chromium | 设置 ALL_BROWSERS=1 后追加 firefox 与 webkit |
trace / screenshot |
on-first-retry / only-on-failure |
失败诊断资产 |
测试前会由 webServer 自动执行 pnpm dev:test,并以轮询 http://localhost:7175/comments-ui.min.js 是否可访问作为就绪信号(reuseExistingServer: !process.env.CI),因此本地重复运行时不会反复重启服务。
针对不同浏览器的运行变体:
pnpm test:acceptance # chromium + headless
pnpm test:acceptance:slowmo # TIMEOUT=100000 PLAYWRIGHT_SLOWMO=1000,带 --headed 供人工观察调试
pnpm test:acceptance:full # ALL_BROWSERS=1,三个浏览器全量跑
现有 e2e 用例覆盖了完整用户链路,例如 test/e2e/content.test.ts(评论列表渲染)、comment-submission.test.ts(发表评论)、threads.test.ts 与 pagination.test.ts(楼中楼与分页)、reply-refetch.test.ts(回复后拉取)、permalink.test.ts(深链滚动定位)、lazy-loading.test.ts(懒加载行为)、admin-moderation.test.ts(管理员审核)、disabled-member.test.ts(会员无评论权限)、editor.test.ts(富文本编辑器)与 cta.test.ts(登录/升级引导)等。
Lint
pnpm lint # lint 全量 = lint:code + lint:types
pnpm lint:code # eslint src --cache
pnpm lint:types # 即 test:types
发布机制:自动 patch 与手动 minor/major
Patch 版本:全自动
README 明确了关键事实:patch 发布是自动的。当 Comments UI 的改动合入 main 分支后,CI 会自动计算并发布下一个 patch 版本到 npm,并清除 jsDelivr CDN 缓存。由于服务端只把版本锁定在 major.minor(当前为 ~1.6,见 ghost/core/core/shared/config/defaults.json 的 comments.version),使用该版本行的线上站点无需等待 Ghost 发版即可自动获得 patch 修复——这也是为何阅读 package.json 时不会看到显式的 patch release 脚本:它完全交给 CI 处理。
Minor / Major 版本:pnpm ship 手动驱动
需要刻意发布 minor 或 major(例如引入破坏性改动或新增能力)时,执行:
pnpm ship
pnpm ship 实为运行 node ../../scripts/release-apps.js,发布脚本逻辑位于 scripts/release-apps.js,README 给出的三步流程与脚本内部校验一一对应:
- 从干净的 feature 分支运行
pnpm ship,按提示选择 minor 或 major; - 将 release commit 合入
main; - 等待一次公开的 Ghost 版本发布,把新的默认版本行推给所有站点。
脚本内部的硬性约束包括:
ensureNotOnMain():禁止在main分支上直接发版,必须基于干净分支操作;ensureCleanGit():存在未提交的本地改动时中止,避免把半成品打包进 release commit;- 版本类型只允许
minor或major(patch 提示为自动发布,不由本脚本承担),交互式输入默认 minor; - 同步更新默认版本行:
updateConfig()会将defaultConfig[app.configKey].version写为majorMinor(newVersion)。shared 词表见 scripts/lib/public-apps.js——comments-ui包的配置键被 camelCase 为commentsUi,但在 Ghost 默认配置(defaults.json)中对应键却是comments;脚本注释强调,package.json 与默认版本行必须保持同步(lockstep),否则仓库内的check-app-version-bump检查会直接让 PR 失败——因为线上站点会指向一个尚未发布的版本线。
发布前的门禁
package.json 中定义了 preship: pnpm lint,即执行 pnpm ship 前会自动先跑完整 lint(含类型检查),任何告警/类型错误都会阻塞发版流程;同时 prepublishOnly: pnpm build 确保推送到 npm 的包一定包含最新构建产物。整套链条可概括为:
- 改动合入
main→ CI 自动 patch + 清 CDN 缓存(线上同 major.minor 站点秒级升级); - 手动
pnpm ship(minor/major)→ 更新 npm 版本 + 改写 Ghost 默认版本行 → 合入main→ 随下一次公开 Ghost 发布,全站切到新版本线。
写在最后:定位与许可
作为 Ghost 三大 public UMD app(Comments、Portal、Announcement Bar 等并列于 ghost/core/core/shared/config/defaults.json)之一,Comments UI 是「版本随服务端配置、产物走 CDN、发布独立于 Ghost」的典型样例。开发者既可以用 pnpm dev:public 把它放入真实 Ghost 中调试主题级交互,也可以用 pnpm dev:test + Playwright 单独做端到端回归;任何涉及版本线的操作都要牢记 npm 包版本、defaults.json 的 comments.version 与 CDN 缓存三者必须协同一致。
本包代码版权归 Ghost Foundation(2013–2026),基于 MIT 许可证发布,仓库根目录 LICENSE 为完整许可文本。如需贡献修改,请遵循 README 中的开发与发布流程,并在 PR 中保持版本 bump 与默认配置同步。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00