首页
/ Ghost Comments UI 开发与发布全指南:从评论区嵌入式组件到 jsDelivr 版本管理

Ghost Comments UI 开发与发布全指南:从评论区嵌入式组件到 jsDelivr 版本管理

2026-09-07 23:21:10作者:郜逊炳

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>

其中 scriptUrlgetFrontendAppConfig('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.jsversion1.6(默认锁定 major.minor 一行,即允许 CDN 在该行内自动升级 patch,对应 README 中「同一 major/minor 行的站点自动获得 patch」的说法);
  • 配置读取时用 url.replace('{version}', appVersion) 把版本号填入 URL 模板;
  • 组件侧则把 data-ghost-commentsdata-apidata-keydata-post-iddata-color-schemedata-avatar-saturationdata-accent-colordata-comments-enableddata-countdata-titledata-publicationdata-localedata-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/ 目录,这是 npm files 白名单(umd/LICENSEREADME.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 自举」的过程:

  1. 通过 document.currentScript 找到注入的 <script data-ghost-comments> 标签(ESM 开发模式下降级为 querySelector);
  2. 在脚本标签之前插入并复用容器 <div id="ghost-comments-root">。根节点 ID 定义于 apps/comments-ui/src/utils/constants.ts,注释特别说明:不能使用 ghost-comments 作为 ID,否则会破坏页面加载后注入 div 导致的 #ghost-comments 锚点滚动;
  3. 从 URL hash 解析 initialCommentId,用于深链定位单条评论;
  4. 清理 URL 上的 ?token= 参数(避免把一次性鉴权令牌留在地址栏);
  5. 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.vendornavigator.platformnavigator.maxTouchPoints)替换为基于 navigator.userAgent 的等价判断。原因是该 UMD 通过 cdn.jsdelivr.net 分发,而被 DuckDuckGo Tracker Radar 及 Safari 26+ 的 Advanced Fingerprinting Protection 视为指纹风险域的脚本会受限。插件 enforce: 'pre',在 transform 阶段对 prosemirror-viewprosemirror-keymapprosemirror-commandsw3c-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.tsxcomponents/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.tspagination.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.jsoncomments.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 给出的三步流程与脚本内部校验一一对应:

  1. 从干净的 feature 分支运行 pnpm ship,按提示选择 minor 或 major;
  2. 将 release commit 合入 main
  3. 等待一次公开的 Ghost 版本发布,把新的默认版本行推给所有站点。

脚本内部的硬性约束包括:

  • ensureNotOnMain():禁止在 main 分支上直接发版,必须基于干净分支操作;
  • ensureCleanGit():存在未提交的本地改动时中止,避免把半成品打包进 release commit;
  • 版本类型只允许 minormajor(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.jsoncomments.version 与 CDN 缓存三者必须协同一致。

本包代码版权归 Ghost Foundation(2013–2026),基于 MIT 许可证发布,仓库根目录 LICENSE 为完整许可文本。如需贡献修改,请遵循 README 中的开发与发布流程,并在 PR 中保持版本 bump 与默认配置同步。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388