ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Ghost Koenig 默认卡片包 @tryghost/kg-default-cards 解析:Mobiledoc 卡片的定义、渲染与 URL 变换机制

Ghost Koenig 默认卡片包 @tryghost/kg-default-cards 解析:Mobiledoc 卡片的定义、渲染与 URL 变换机制 Ghost Koenig 默认卡片包 tryghost/kg-default-cards 解析Mobiledoc 卡片的定义、渲染与 URL 变换机制【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文围绕 Ghost 编辑器 Koenig 生态中承担历史渲染职责的tryghost/kg-default-cards位于仓库 koenig/kg-default-cards展开先厘清它在 Mobiledoc 时代编辑器与 ghost/core 之间的定位再逐层剖析卡片的统一接口name/type/render、基于 SimpleDOM Handlebars 的渲染路径、存储/读取两侧的 URL 变换辅助函数以及针对邮件客户端Outlook 等的特殊输出逻辑。读完本文你将掌握如何安装该包、如何通过cards数组驱动 Mobiledoc 渲染器输出完整 HTML以及 Ghost 对每张卡片内嵌 URL 做绝对/相对地址转换的实现原理与验证方式。一、包定位Mobiledoc 时代遗留的卡片定义集tryghost/kg-default-cards的官方描述是 Mobiledoc card definitions for Ghosts editorpackage.json。在 Ghost 编辑器 Koenig 的演进中卡片渲染逻辑经历了两次重大更替Lexical 时代当前主线所有新编辑工作都在koenig/koenig-lexical与koenig/kg-default-nodes中完成。其中kg-default-nodes是节点渲染的唯一事实来源编辑器与服务端都经由它渲染Mobiledoc 时代本包所代表kg-default-cards是供Mobiledoc renderer消费的卡片定义集合与koenig/kg-card-factory卡片定义工厂一起被ghost/core消费用于渲染从未被转换到 Lexical 格式的存量 Mobiledoc 文章。这一点在 koenig/README.md 的 Legacy (Mobiledoc era) 分类表里写得很明确Still consumed byghost/coreto render posts that have never been converted from Mobiledoc. Avoid new work here.因此本文所述的 API 属于维护模式legacy能力是新代码不应再扩展、但存量内容仍然依赖的渲染底座。阅读时若关注的是新编辑器能力应转向 koenig/kg-default-nodes 与 koenig/koenig-lexical若关注的是 Mobiledoc 与 Lexical 互转则可参考 koenig/kg-converters。二、安装外部 npm 与 monorepo 两种途径作为对外发布的工作区包它支持 npm 直接安装npm install tryghost/kg-default-cards与此同时包本身属于 Ghost monorepopnpm workspace因此仓库内开发并不需要任何 linking 或逐包 install。在 monorepo 根目录执行一次pnpm setup后即可直接在koenig/kg-default-cards目录内开展工作所有依赖如tryghost/url-utils、tryghost/kg-markdown-html-renderer等均通过workspace:~/catalog:版本约束解析详见 koenig/README.md 的 Development 章节。从 package.json 还能读到包的工程细节主入口CJS 与 ESM 双构建main指向build/cjs/index.jsmodule与types指向build/esm/index.js/index.d.tsexports中source条件指向原始src/index.ts使 ghost/core 的开发模式无需tsc重编译即可热加载源码修改运行时依赖handlebars模板、juice内联 CSS、luxon时间处理、tryghost/url-utilsURL 变换、tryghost/kg-markdown-html-renderermarkdown 卡片与tryghost/kg-utils、tryghost/stringNode 版本要求^22.13.1 || ^24.0.0。三、卡片清单与统一接口包的入口 src/index.ts 只做两件事导出cards数组并导出Card、CardRenderArgs、CardRenderEnv、CardRenderOptions、UrlTransformOptions等类型。3.1 二十种默认卡片src/cards/index.ts 汇总了全部 20 个默认卡片。README 中演示了枚举卡片名import {cards} from tryghost/kg-default-cards; cards.map(card card.name); // [bookmark, code, email, email-cta, embed, ...]以仓库源码为准完整清单与cards数组顺序一致为[ bookmark, // 链接书签 code, // 代码块 email, // 邮件订阅按钮inline 邮件卡片 email-cta, // 邮件 CTA embed, // 通用嵌入含 twitter/nft 子实现 gallery, // 图片画廊 hr, // 分割线 html, // 自定义 HTML image, // 图片 markdown, // Markdown paywall, // 付费墙 button, // 按钮 callout, // 提示框 product, // 产品卡片 toggle, // 折叠 audio, // 音频 video, // 视频 file, // 文件下载 header, // 标题块 before-after // 前后对比 ]每张卡片对应一个独立源码文件位于 src/cards/其中embed的 Twitter 与 NFT 变体拆分为 embed/twitter.ts 与 embed/nft.ts。3.2 Card 接口契约src/types.d.ts 定义了卡片的统一契约export interface Card { name: string; type: string; config?: Recordstring, unknown; render(args: CardRenderArgs): SimpleDomNode; // URL 变换辅助仅含 URL 的卡片实现 absoluteToRelative?(payload, options): Recordstring, unknown; relativeToAbsolute?(payload, options): Recordstring, unknown; toTransformReady?(payload, options): Recordstring, unknown; }name卡片类型名与 Mobiledoc 文档中卡片数据的第一项对应type卡片渲染类型。目前各卡片实现如bookmark、image、code、embed一律声明为type: dom即渲染结果是一个 DOM 节点而非纯字符串由 Mobiledoc renderer 直接挂载render(args)核心渲染函数返回一个SimpleDomNode可直接交给 Mobiledoc renderer 输出可选 URL 变换函数只有那些内部携带外部 URL 的卡片如bookmark的url/metadata、image的src以及含 caption 的code/embed/email等才实现这三个方法供 Ghost 在存储内容与向前台提供内容时统一改写地址。README 中ready to hand to the Mobiledoc renderer指的就是把Card[]数组整体交给渲染器后renderer 会按文档中的卡片 name 找到对应定义并调用其render。四、渲染机制render 参数与输出形式render接收的CardRenderArgs由 payload、env、options 三部分组成src/types.d.tsexport interface CardRenderArgs { payload: Recordstring, unknown; // 卡片在文章中的持久化数据JSON env: CardRenderEnv; // { dom: SimpleDomDocument } options?: CardRenderOptions; // 渲染上下文 }4.1 payload卡片的持久化数据payload是文章存储结构中卡片的负载对象例如 image 卡片为{ src, width, height, cardWidth?, caption?, alt?, title?, href? }image.ts。渲染空负载时卡片必须返回空文本节点dom.createTextNode()——这是各卡片在开头普遍执行的防御逻辑例如 image 无src、bookmark 无metadata.title、code 无code时都会安静地输出空内容对应测试见 bookmark.test.ts 中的renders nothing when payload is empty。4.2 两种 DOM 构建方式Mobiledoc renderer 并不直接操作浏览器 DOM而是传入一个SimpleDOM文档对象env.dom。卡片据此有两种写法方式一声明式 DOM API。以 code.ts 为例它依次创建pre、code节点并拼接const pre dom.createElement(pre); const code dom.createElement(code); if (payload.language) { code.setAttribute(class, language-${payload.language}); } code.appendChild(dom.createTextNode(payload.code)); pre.appendChild(code);带 caption 时再包一层figure.kg-card.kg-code-card并追加figcaptioncaption 属于用户可控的富文本用createRawHTMLSection原样注入。方式二Handlebars 模板 createRawHTMLSection。需要输出大量结构复杂的 HTML尤其邮件场景时卡片会借助包内工具 src/utils/hbs.ts 把标签模板字面量编译为 Handlebars 模板函数export default function hbs(literals, ...values) { let output ; for (let i 0; i values.length; i) { output literals[i] String(values[i]); } output literals[values.length]; return Handlebars.compile(output); }bookmark 卡片即用该方式生成figure.kg-card.kg-bookmark-card的主模板并通过{{#if}}、{{{caption}}}等语法按 payload 动态输出最终结果经 utils/dedent.ts 去缩进后以dom.createRawHTMLSection(...)注入。同文件里还能看到针对邮件目标options.target email拼接的!--[if !mso !vml]…![endif]--与 VML 条件注释后面第五节详述。4.3 options渲染上下文渲染选项集中体现了卡片渲染的目标驱动特征types.d.ts字段类型作用targetstring渲染目标源码中以email作为切换分支条件如 image/bookmark/embed 都判断target emailsiteUrlstring站点基础 URL用于 URL 相对化/绝对化与本地图片判定itemUrlstring当前内容项的 URL用于相对地址恢复postUrlstring文章 URL部分卡片使用ghostVersionstringGhost 版本标识canTransformImage(src) boolean图片处理许可回调imageOptimization{ srcsets?, contentImageSizes?, defaultMaxWidth? }图片优化配置见第七节五、邮件目标target email 的分支细节多张卡片在options.target email下会切换输出这是本包极具特色的部分——Ghost 需要把同一张卡片的 Mobiledoc 数据同时渲染成网页版与邮件版HTML而邮件客户端尤其是 Outlook能力极为残缺。5.1 bookmark条件注释 juice 内联样式bookmark.ts 注释解释了设计动机Outlook 会把卡片当成注释处理基于 DOM 的工具如juice无法把样式表内联进去因此必须预先把 Outlook 专用模板用juice做内联将style块合并进表格/标签的 style 属性再放进!--[if vml]条件注释中。于是 bookmark 卡片的邮件输出结构为!--[if !mso !vml]-- 常规 HTML现代客户端缩略图用 background-image![endif]-- !--[if vml] juiced 后的 table 布局Outlook/Word 渲染引擎![endif]--对应快照断言见 bookmark.test.ts 的renders email target测试用serializer.serialize(card.render(opts))比对出完整字符串其中能清楚看到 VML 分支里每个td都携带内联style。5.2 embed视频缩略图播放按钮的 VML 版embed.ts 对type video且带metadata.thumbnail_url的嵌入做了专门处理邮件里不直接内嵌 iframe而是按 600px 邮件模板宽度与缩略图宽高比计算占位尺寸输出一个带播放按钮样式的kg-video-preview链接其中用于撑开宽高的 spacer 通过外链 spacer GIF 实现并同时给出 VMLv:group版本的播放按钮以保证 Outlook 可见。embed 卡片还会把payload.type分流给子实现twitter交给 embed/twitter.tsnft且带 metadata交给 embed/nft.ts。六、URL 变换存储与输出两侧的地址改写README 特别强调含有 URL 的卡片还会暴露对应的 transform helpersabsoluteToRelative、relativeToAbsolute、toTransformReadyGhost 在存储与服务内容时应用它们。理解这三个函数的关键背景是Ghost 文章里的内部链接允许以相对路径保存便于站点迁移但在内容输出时必须还原为携带站点域名的绝对地址。以 image.ts 为例实现非常直接absoluteToRelative(payload, options) { p.src absoluteToRelative(p.src, options.siteUrl, options); // src 由绝对改相对 p.caption htmlAbsoluteToRelative(p.caption, options.siteUrl, options); // caption HTML 内 URL 整体处理 return payload; }其底层全部来自tryghost/url-utils见 package.json 依赖card 层只是声明我要变换哪几个字段absoluteToRelative写库/优化内容时把绝对 URL 转为相对 URL适合内部链接、本站图片relativeToAbsolute输出内容时把相对 URL 恢复为绝对 URL带站点域名toTransformReady把内容转换为可变换状态HTML 与纯 URL 处理有所差异。需要变换的字段因卡片而异image 是顶层src与captionbookmark 则涉及payload.url、metadata中的url/icon/thumbnail及caption见 bookmark.ts它对数组化的元数据字段做循环变换embed/code/html/email 等卡片通常只处理 HTML 形态的caption/html字段因为它们把对 URL 的变换委托给htmlAbsoluteToRelative/htmlToTransformReady这类 HTML 级工具。七、图片卡片的响应式与邮箱优化image.ts 是 options 中最完整的例子体现了渲染与图片优化配置的联动。7.1 srcset/sizes网页响应式非邮件目标下渲染会为img生成srcset规则集中在 utils/srcset-attribute.ts默认内容尺寸档位为[600, 1000, 1600, 2400]源码注释标注来自options.imageOptimization.contentImageSizes只对本地内容图片路径形如/content/images/...由 is-local-content-image.ts 判定与Unsplash 图片is-unsplash-image.ts生成 srcset分别输出/content/images/size/w{width}/...形式或追加?w查询参数的 Unsplash URL已有档位与图片原始宽度相等时直接复用原路径避免 302 跳转超过原图宽度的档位一律跳过保证绝不产出比原图更大的 srcset 条目只有当srcset存在且图片宽度 ≥ 720 时才追加sizes属性标准卡 720px、wide 卡 1200pxoptions.imageOptimization.srcsets false或canTransformImage返回 false 时不生成 srcset。7.2 邮件邮箱优化分支target email下的规则与其相反邮件客户端对srcset/sizes支持差一律不加srcsetOutlook 无法自动缩放图片因此显式输出 width/height 属性并把超过 600px 的图统一缩到 600px邮件模板最大宽度为了在 retina 屏上仍清晰会寻找 ≥ 1200px 的内容图片档位size/w1200/仅在原图更宽时替换 src无宽高元数据、直接来自 Unsplash 的图片强制把w参数设为 1200避免 Outlook 收到超大图。当defaultMaxWidth存在且图片宽度超过它同时为本地图片且允许变换时还会用 utils/resize-image.ts 等比计算缩放后的 width/height 输出保证第三方画廊插件拿到一致的尺寸。八、测试与质量保障README 列出了包级命令定义见 package.json命令作用pnpm test:unit运行 Vitest 单元测试含覆盖率pnpm test运行全部配置的测试unit types包含覆盖率阈值检查pnpm lintESLint 检查作为posttest自动执行测试仓库采用快照式断言模式用simple-dom的Document构造渲染环境再经HTMLSerializer把render()产物序列化为字符串后与期望 HTML 完全相等比对。以 bookmark.test.ts 为例import card from ../../src/cards/bookmark.js; const serializer new HTMLSerializer(voidMap); it(renders, function () { const opts { env: { dom: new SimpleDomDocument() }, payload: { url: http://example.com, metadata: { /* ... */ } }, }; expect(serializer.serialize(card.render(opts))).toBe( figure classkg-card kg-bookmark-card ……/figure, ); });这种断言方式可以精确锁定类名kg-card、kg-image-card、kg-bookmark-card、空负载输出、href取payload.url而非metadata.url等行为主题卡片样式依赖的 CSS 类名因此被稳定约束。全量测试覆盖src/cards/下绝大多数卡片bookmark、code、image、embed、email、gallery、html、video、toggle、paywall、product等均各有独立测试文件以及 utils/ 侧的图片辅助函数。与其它 kg-* 包一致其 Vitest 基座来自 koenig/vitest.shared.ts详见 koenig/README.md。九、构建与发布形态由于 Ghost 本体从不从 npm 安装这些包koenig/README.md本包的发布版本只服务外部消费者开发/CI通过 workspace 解析源码发行归档ghost/core/scripts/pack.js将 kg-* 包作为组件 tarball 递归打进部署产物且files不含src/原始源码不随包发布构建脚本tsc先产出 ESM 类型与声明再以tsconfig.cjs.json产出 CJS并写入{type:module}标记文件区分两种模块系统npm 发布由仓库级 Publish Packages 工作流在稳定版 release tag 上批量发布并改写workspace:版本范围。十、结语tryghost/kg-default-cards是理解 Ghost 内容渲染历史的关键切面它展示了把结构化卡片数据渲染为多种目标 HTML的成熟范式——用统一Card契约收敛 20 种卡片用 SimpleDOM 抽象屏蔽宿主 DOM 差异用 URL 变换三函数保证内容在相对/绝对地址间的无损切换并用条件注释与内联样式对抗邮件客户端的兼容性黑洞。虽然新功能开发已迁移至 Lexical 体系但只要存量 Mobiledoc 文章仍在 Ghost 中提供服务本包就仍是 ghost/core 渲染链路中不可缺席的一环。相关可继续阅读的资料包括共享开发/测试/发布流程说明 koenig/README.md、卡片工厂 koenig/kg-card-factory、渲染依赖的 koenig/kg-lexical-html-renderer 及当前 Lexical 节点的定义包 koenig/kg-default-nodes。包本身采用 MIT License 发布。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表