product Software Technology

KuroCMS开发记 Vol.5:食谱专用类型功能实现

根据用户反馈,我们在KuroCMS中实现了全新的食谱卡片功能。了解其技术架构、JSON-LD生成以及编辑器集成的详细细节。


在作为边缘运行的下一代无头CMS开发的“KuroCMS”及“KuroEditor”中,最新版本已正式追加实现了全新的“食谱卡片”。以往的KuroCMS主要侧重于使用Markdown或标准HTML的通用文档表达,而此次食谱卡片的引入能够严格处理烹饪时间、材料列表、人数、步骤等元数据,确保一贯输出为谷歌等搜索引擎易于理解的结构化数据(Schema.org Recipe)。在本文中,我们将详细介绍该食谱卡片的技术设计,并展示如何在编辑器、API和前端层之间彻底执行校验。

设计思想与基本设计:限制为1篇文档=1个食谱的智能数据模型

在实现食谱卡片时,开发团队面临的最大挑战是“数据一致性与消除冗余”。在普通的食谱发布网站中,一篇文章中往往会零散分布多个食谱卡片,但从搜索引擎结构化数据(JSON-LD)的角度来看,这很容易生成破损的结构。如果针对单个URL(文章)并行输出多个不同的菜肴或步骤,则文章公共属性(如菜名、成品图、说明和发布日期)与单个食谱卡片内数据之间的对应关系就会变得模糊不清。

因此,KuroCMS强制执行“1篇文档=1个食谱”的规则限制。食谱卡片(RecipeCard)仅负责人数、烹饪时间、材料和步骤等“食谱特有的可变部分”,而菜名、说明、成品图和发布日期等公共信息则复用文档属性(标题、摘要、封面图、创建日期等)。这避免了重复输入,并防止了相同食谱名称在同一结构化数据内以破损格式循环的问题。

老实说,从KuroEditor的设计角度来看,我们并不想接受1篇文档=1个食谱的限制,因为这限制了排版的灵活性。然而,由于用户的食谱页面能获得更高的SEO评价,且更易于阅读和引用,我们在现行版本中严格执行每篇文章仅限1张食谱卡片的规则。

通过共享纯函数保证一致性

在无头CMS中,在富文本编辑器(KuroEditor)、保存API以及公共页面的静态构建过程这三个地方执行完全相同的数据结构验证并非易事。如果使用不同的语言或框架在各层分别编写验证逻辑,实现上的偏差将不可避免地导致问题,例如“在编辑器中保存成功但在API服务器报错”,或者“已保存到数据库但在前端构建时报错,导致网站损坏”。

为了从根本上解决这个问题,KuroCMS采用了“共享纯函数(Shared Pure Function)”的设计模式。具体而言,我们将上游KuroEditor仓库中构建的通用食谱处理模块“dist/kuro-recipe.js”引入KuroCMS核心作为“src/kuro-recipe.js”,并配合类型定义文件“kuro-recipe.d.ts”。这确保了在编辑器编辑、保存API调用和静态构建时,完全相同的JavaScript代码以相同的验证标准检查正文HTML中的食谱数据。这种单一光源(Single Source of Truth)设计将Bug减少到最低限度。

*由于KuroEditor和KuroCMS都是由黑兔开发的“开源软件”,大家一起参考GitHub的历史记录会更容易理解。

保存API中的严格验证:recipe-guard.ts规范

在KuroCMS中保存(PUT)文章时,服务器端API不仅存储字符串,还会严格检查发送的正文HTML中是否正确嵌入了食谱数据。这一角色由后端“src/recipe-guard.ts”中的“checkRecipeCards(bodyHtml, isRecipeType)”函数承担。

该校验最关键的方面是其执行时机。KuroCMS考虑了多用户协同编辑,最终保存的数据是在对多个版本进行“3路合并”后写入数据库的。如果在合并前运行校验,则无法防止合并意外复制食谱卡片或插入损坏数据的情况。因此,检查总是针对合并后的最终正文执行。如果校验失败,API会向客户端返回明确的错误,阻止保存过程。

检查项目 具体检查内容与安全措施
食谱卡片数量 确认食谱文章中必须正好存在1张卡片,而在非食谱文章中必须存在0张卡片。
data-recipe解密 解密HTML标签中嵌入的已编码食谱JSON数据,并检查其是否符合Schema定义。
属性白名单(allowlist) 严格检查未知的HTML属性。这彻底消除了恶意用户通过HTML正文注入“onclick”等XSS脚本的漏洞。
版本兼容性检查 检查食谱Schema的版本兼容性,防止保存旧卡片版本或损坏的格式。

JSON-LD输出规范:自动生成用于SEO的Recipe结构

这是添加食谱卡片的核心价值所在。通常,KuroCMS输出“Article”作为页面的结构化数据,但当文档包含食谱卡片时,它会自动进行替换,并输出“Recipe”作为主要的结构化数据。

它从保存的HTML中读取“data-recipe”属性,并将解析后的数据作为“article.recipe”和“page.isRecipe”传递给HTML模板。为了避免在模板中进行“通过字符串比较进行类型检查”,它使用简单的布尔值分支(#if page.isRecipe)来切换模板。时间数据会在内部自动转换为国际标准的ISO-8601 duration格式(例如,25分钟为“PT25M”)。由于HTML展示和JSON-LD结构化数据是从完全相同的单一数据源生成的,因此不存在搜索引擎信息不匹配的风险。

后台集成:动态编辑器重构逻辑

后台管理面板的编辑器区域也构建了集成逻辑。在KuroEditor中,用于创建食谱的“锅按钮”的启用与禁用是由初始化时传递的“recipeUi”选项控制的。由于KuroEditor的默认行为是显示该按钮(true),因此KuroCMS通常不需要指定它,但如果设置为false,对于不使用KuroEditor编辑食谱的用户来说,它看起来没有任何变化,从而将更改的影响降到最低。

此外,在KuroEditor级别,食谱卡片现在被锁定为每篇文章仅限1个食谱(第二个按钮置灰且无法点击)。这维护了食谱文章的核心规则。KuroCMS还会对保存进行双重检查,以确保食谱专用检查针对食谱文章正确运行。

基于此,新增了食谱相关的测试套件“npm run test:recipe”(src/recipe-guard.test.ts,共计12个测试用例),校验了从卡片数量到属性中字符转义的所有内容。

遗留问题与未来路线图

通过此次更新,食谱文章的“核心系统”和“保存验证”已完全投入运行。然而,KuroCMS目前还无法提供像大型门户网站那样的大规模食谱页面布局。我们的目标是允许在个人网站上轻松发布食谱,自动生成SEO数据而无需用户编写。现在任何人都可以轻松开通烹饪网站了。

此外,由于KuroCMS包含模板系统,用户可以继续自行完善设计(详情请见来源3)。为了让KuroCMS更加易于使用,开发团队将继续根据您的反馈实施更新,感谢大家的支持!



【出典】

  1. KuroCMS 官方代码库与实现规范
  2. Schema.org Recipe 结构化数据 (JSON-LD) 官方定义
  3. 公开模板文档 5:食谱专用卡片 (RECIPE)