首页
/ Ghost Koenig kg-default-nodes 全解析:卡片 Lexical 节点定义与统一 HTML 渲染的事实源

Ghost Koenig kg-default-nodes 全解析:卡片 Lexical 节点定义与统一 HTML 渲染的事实源

2026-09-07 17:04:27作者:凌朦慧Richard

kg-default-nodes(@tryghost/kg-default-nodes)是 Ghost Koenig 编辑器体系中所有卡片(Card)的 Lexical 节点定义包,同时为每个节点提供了 HTML 渲染器。它把"编辑器侧编辑数据"与"服务器侧渲染正文"统一到同一套节点实现上,是 Ghost 帖子正文(Editor → 存储 → 渲染)全链路的事实源(single source of truth)。读完本文,你将掌握它的安装集成方式、DEFAULT_NODES / DEFAULT_CONFIG 的用法、卡片节点的声明式定义机制,以及它在编辑器与服务器两侧的底层调用链路。

包定位:节点定义与渲染的双重职责

kg-default-nodes/README.md 中,这个包被定义为:Ghost 全部卡片的 Lexical 节点定义 + 每个节点各自的 HTML renderer。它有两个关键设计约束:

  1. 单一事实源:编辑器(前端)和服务器(后端)在渲染正文时都经过这一套节点,避免前后端各写一套渲染逻辑导致的不一致。
  2. 必须保持浏览器安全(browser-safe):该包既运行在编辑器内部(浏览器环境),也会运行在服务器上。因此源码中不能引入依赖 DOM/BOM 全局对象、不能在模块顶层访问 window/document 的代码。例如 export-dom.tsdom 选项就是通过 {window: {document: Document}} 这样可注入的形式访问文档对象的。

从源码目录结构可以更直观地看到它的职责分层(见 src 目录):

  • nodes/:每个卡片一个子目录,如 image/callout/signup/email-cta/ 等,内含节点类、*-parser.ts(HTML → 节点)与 *-renderer.ts(节点 → HTML);
  • serializers/:粘贴/导入 HTML 时把基础标签映射到节点的序列化器(linebreak.tsparagraph.ts);
  • KoenigDecoratorNode.ts:所有卡片节点共用的基类与卡片契约;
  • generate-decorator-node.ts:批量生成 Decorator 节点类的工厂函数;
  • visibility.ts:会员可见性(Visibility)的旧/新格式解析与迁移工具。

安装与工程环境

作为独立 npm 包,安装命令为:

npm install @tryghost/kg-default-nodes

但在 Ghost monorepo 内部开发时并不需要单独安装——它通过 pnpm workspace 解析。开发步骤为:先在仓库根目录执行 pnpm setup,然后直接在 koenig/kg-default-nodes 目录内工作即可(参见 kg-default-nodes/README.md 的 "Develop" 一节)。相关共享的构建、测试、发布流程统一在 Koenig README 中说明。

package.json 可以看到这个包的工程细节:

  • 双模块产物main 指向 build/cjs/index.js(CommonJS),moduletypes 指向 build/esm/index.jsbuild/esm/index.d.tsexports 字段中还对 ./visibility 做了子路径导出;
  • Node 版本要求engines 声明为 ^22.22.2 || ^24.15.0
  • 关键依赖:Lexical 生态固定为 0.13.1lexical@lexical/clipboard@lexical/rich-text 等),并通过 workspace:~ 依赖同一仓库中的 @tryghost/kg-clean-basic-html@tryghost/kg-markdown-html-renderer
  • 发布内容files 只包含 LICENSEREADME.mdbuild 目录。

核心用法:DEFAULT_NODES 与 DEFAULT_CONFIG

README 给出了最小集成示例:

const {createEditor} = require('lexical');
const {DEFAULT_NODES, DEFAULT_CONFIG} = require('@tryghost/kg-default-nodes');

const editor = createEditor({
    nodes: DEFAULT_NODES,
    html: DEFAULT_CONFIG.html
});

DEFAULT_NODES:开箱即用的卡片节点集合

DEFAULT_NODES 是一个完整的节点类数组,覆盖了 Ghost 自有卡片。在 kg-default-nodes.ts 中可以看到它包含:

  • 基础扩展节点ExtendedTextNodeExtendedHeadingNodeExtendedQuoteNode(连同各自的 Replacement 变体,用于替换 Lexical 默认节点以支持 Koenig 的额外行为);
  • 内容与媒体卡片ImageNodeVideoNodeAudioNodeGalleryNodeBookmarkNodeFileNodeCodeBlockNodeMarkdownNodeEmbedNodeHtmlNodeHorizontalRuleNode
  • 会员转化卡片SignupNodeEmailCtaNodeEmailNodeCalloutNodeCallToActionNodeProductNodeButtonNodeToggleNodeHeaderNodeAsideNodePaywallNodeTransistorNode
  • 内部辅助节点TKNode(占位)、AtLinkNode / AtLinkSearchNode(@ 提及)、ZWNJNode(零宽不连字符处理)。

需要强调的是,DEFAULT_NODES 只覆盖 Ghost 自有卡片。它不包含基础的 ParagraphNodeTextNodeHeadingNode 等标准节点,因此 README 提醒:要把它与你的内容真正需要的 Lexical 基础节点搭配使用。

DEFAULT_CONFIG.html:粘贴内容导入的序列化器

DEFAULT_CONFIG.html 提供的是导入侧(import)的序列化器,作用是让用户粘贴进来的 HTML 能被正确映射到对应节点。其构成见 kg-default-nodes.ts

export const serializers = {
    linebreak: linebreakSerializers,
    paragraph: paragraphSerializers
};

export const DEFAULT_CONFIG = {
    html: {
        import: {
            ...serializers.linebreak.import,
            ...serializers.paragraph.import
        }
    }
};

即把 serializers/linebreak.tsserializers/paragraph.ts 中的 import 序列化器合并而成。这些序列化器往往针对真实场景做了细致处理,例如 paragraph.ts 会识别 Google Docs 用 docs-internal-guid- 包裹的空段落并直接丢弃,避免把 Google Docs 复制内容转成编辑器里一堆空段落:

import: {
    p: (node) => {
        const isGoogleDocs = !!node.closest('[id^="docs-internal-guid-"]');
        if (isGoogleDocs && node.textContent === '') {
            return {conversion: () => null, priority: 1};
        }
        return null;
    }
}

按需引入单个节点

并不是所有场景都需要全集。当只需要部分节点时,包会按名称逐个导出——ImageNodeCalloutNodeSignupNode 等都可以单独 import(导出清单见 kg-default-nodes.ts)。同时每个节点通常还配有 $createXxxNode$isXxxNode 的 type guard 辅助函数,例如 ImageNode.ts 中的 $createImageNode(dataset)$isImageNode(node)

其它同包导出:utils 与 visibility

kg-default-nodes.ts 可以看到包还聚合导出了若干实用工具(utils 对象):generateDecoratorNodevisibilityrgbToHextaggedTemplateFnsreplacementStrings,方便其它包与测试复用。此外 card-widths.ts 也随包导出,定义了卡片宽度枚举 ['regular', 'wide', 'full']isCardWidth / normalizeCardWidth 校验函数(见 card-widths.ts)。

会员可见性工具还可通过子路径 @tryghost/kg-default-nodes/visibility 单独引入(package.json),其中定义了各会员分段的 NQL 常量与格式迁移逻辑(详见 visibility.ts):

  • ALL_MEMBERS_SEGMENT = 'status:free,status:-free'(全部会员)
  • PAID_MEMBERS_SEGMENT = 'status:-free'(付费会员,含 comped 与 gift)
  • FREE_MEMBERS_SEGMENT = 'status:free'(免费会员)
  • NO_MEMBERS_SEGMENT = ''(无会员限制)

源码级深入:卡片节点如何被"声明式"定义

几乎全部卡片节点都由 generateDecoratorNode 工厂生成,而不是手写一个完整的 Lexical DecoratorNode 子类。这在 generate-decorator-node.ts 中实现,其核心选项包括:

选项 说明
nodeType 节点的唯一类型名(如 imagecallout),运行时校验不可为空
properties 节点数据字段声明,每个属性需含 defaulturlType 只能是 url/html/markdownwordCount 用于标记是否计入字数统计
version 序列化数据结构版本,默认 1
hasVisibility 是否在节点上加入会员可见性(visibility)属性
defaultRenderFn 默认渲染函数,返回与 @tryghost/kg-lexical-html-renderer 兼容的对象(形如 {element: ..., type: 'inner'/'outer'/'value'/'html'}

以最基础的 ImageNode.ts 为例,属性声明如下:

const imageProperties = {
    src: {default: '', urlType: 'url'},
    caption: {default: '', urlType: 'html', wordCount: true},
    title: {default: ''},
    alt: {default: ''},
    cardWidth: {default: 'regular' as CardWidth},
    width: {default: null as number | null},
    height: {default: null as number | null},
    href: {default: '', urlType: 'url'}
};

export class ImageNode extends generateDecoratorNode({
    nodeType: 'image',
    properties: imageProperties,
    defaultRenderFn: renderImageNode
}) {
    /* @override */
    exportJSON() {
        // src 为 data: base64 时输出占位符 <base64String>,避免巨型数据被序列化入库
        ...
    }
    static importDOM() {
        return parseImageNode(this);  // 来自 image-parser.ts
    }
}

generateDecoratorNode 在生成类之前还会做参数校验(validateArguments):nodeType 必须唯一存在、每个属性必须带 defaulturlType 必须是三种合法值之一、wordCount 必须是布尔值,任何不满足都会在开发期直接抛出带 [generateDecoratorNode] 前缀的错误,提前暴露问题(见 generate-decorator-node.ts)。

卡片契约:KoenigDecoratorNode 与 $isKoenigCard

所有生成/手写的卡片节点都继承自 KoenigDecoratorNode.ts 中的 KoenigDecoratorNode(它自身继承 Lexical 的 DecoratorNode)。KoeningCard 类型定义了一套统一的卡片接口,包含:

  • isKoenigCard(): truehasEditMode():标记是否为 Koenig 卡片、是否支持编辑模式;
  • exportDOM(editor, options):把节点导出为 DOM,即渲染入口;
  • getDataset():返回节点的数据集合;
  • hasDynamicData()getDynamicData?():判断/获取需要异步拉取的动态数据(如 embed 的预览信息);
  • getIsVisibilityActive():判断当前可见性是否激活。

配套的 $isKoenigCard(node) 类型守卫会逐项检查这些方法是否存在且返回正确,从而把"任意 Lexical 节点"安全地收窄为"Koenig 卡片"。

渲染选项:export-dom 的类型骨架

export-dom.ts 定义了渲染相关的全部类型骨架,也是理解渲染上下文(render options)的关键:

  • ExportDOMOutputType'inner' | 'outer' | 'value' | 'html',表示渲染结果以何种形式挂回 DOM;
  • ExportDOMNodeRenderers:渲染器可以是单个函数,也可以是按版本(Record<string|number, renderer>)组织的版本化渲染器——这解释了为什么 Header 等迭代过多个版本的卡片能同时支持 v1/v2 的差异化渲染;
  • ExportDOMOptionsBase:服务器渲染时最常用的字段有 target(渲染目标,如 email/web)、postUrlsiteUrlsiteUuidimageBaseUrlimageOptimizationcanTransformImagecanTransformImageToFormatfeaturedesign 等,且支持 [key: string]: unknown 的开放扩展。

实际落地:编辑器与服务器如何共用这一套节点

"同一套节点既在编辑器跑、也在服务器跑"不是一句口号,而是有明确的调用证据。

服务器侧ghost/core/core/server/lib/lexical.js 中通过 require('@tryghost/kg-default-nodes') 取出 DEFAULT_NODES 注入 LexicalHTMLRenderer(来自 @tryghost/kg-lexical-html-renderer),从而把帖子正文序列化数据渲染为最终 HTML。同一文件中的 buildRenderOptions 还负责构造上面提到的渲染选项——把 site_uuid、站点 url、图片 base URL、imageOptimization 配置,以及 canTransformImage 之类的适配器能力注入渲染流程(见 lexical.js),这与 ExportDOMOptionsBase 的字段一一对应。

编辑器侧:在 Admin 前端中,Koenig 编辑器通过 koenig-loader / koenig-editor-base.tsx(见 apps/admin/src/settings/components 相关组件)加载节点配置并以 DEFAULT_NODES 作为默认节点集合,用户每次对卡片的编辑、预览、拷贝粘贴都会实时走到相同的节点类与 renderer 上。

这意味着:你在一篇文章里插入的图片、会员专享内容(paywall)、Signup/Email 卡片等,无论在编辑器的实时预览里,还是在服务器最终生成网页/邮件时,渲染行为都由 koenig/kg-default-nodes/src/nodes/* 中同一份代码决定,不会出现"所见"与"所得"两套实现漂移的问题。

开发与测试

在 monorepo 内开发该包时,常用命令(源自 package.json 的 scripts,与 README 描述一致):

命令 作用
pnpm dev tsc --watch 监听编译,开发时持续产出构建产物
pnpm build 清理后编译 ESM 与 CJS 双产物,并为 ESM 目录写入 {"type":"module"}
pnpm test:unit NODE_ENV=testing 运行 Vitest 单元测试,并开启覆盖率统计
pnpm test:types tsc --noEmit 运行类型测试
pnpm test 依次运行 test:unittest:types(含覆盖率阈值)
pnpm lint 分别对 src/test/ 执行 ESLint

测试组织与节点结构一一对应:test/renderers/ 下每个卡片都有一个渲染器测试,例如 image-renderer.test.tscallout-renderer.test.tsheader-v1-renderer.test.tsheader-v2-renderer.test.ts 等(见 test/renderers),它们逐个验证节点 exportDOM 产出的 HTML 是否符合预期;另有 test/nodes/test/serializers/test/utils/ 等目录分别覆盖节点数据逻辑、HTML 导入序列化与可见性工具,visibility-smoke.test.ts 则对可见性工具链做冒烟验证。新增一个卡片时,惯例就是同时补齐对应节点的 parser、renderer 与渲染测试。

结语:把"一次定义"贯穿全链路的工程设计

kg-default-nodes 是理解 Ghost 内容体系的一个高质量切入点:卡片节点通过 generateDecoratorNode 声明式生成,属性、版本、可见性与渲染函数高度收敛;DEFAULT_NODES + DEFAULT_CONFIG 让编辑器与服务器共用同一定义与序列化逻辑,从源头避免前后端渲染分裂;visibilitycard-widths、版本化渲染器等导出则把会员功能、版式宽度、历史格式兼容这些真实业务需求沉淀为可复用工具。整个包采用 MIT 许可(见仓库根目录 LICENSE),并随 Ghost monorepo 的 pnpm workspace 统一构建与发布。

如果你正打算深入 Ghost 的编辑器内核、扩展自己的 Koenig 卡片,或研究"一套节点模型同时驱动编辑器与服务器渲染"的工程实践,从 koenig/kg-default-nodes/src 开始阅读是最高效的路径。

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

项目优选

收起
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
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391