EmDash CMS 插件管理后台 UI 与字段组件开发指南沙箱 Block Kit 与原生 React 双路径【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdashEmDash 是一个基于 Astro 的全栈 TypeScript CMS。插件作者可以在不进入浏览器执行沙箱代码的前提下为管理后台贡献导航页面、仪表盘小组件、编辑器侧栏面板、内容操作按钮与声明式字段组件也可以作为站点信任依赖用原生 React 组件深度定制后台。本文以仓库技能文档 admin-ui.md 为主线结合插件清单校验manifest-schema.ts、运行时测试宿主runtime-host.ts与真实插件示例webhook-notifier/emdash-plugin.jsonc完整讲解两条路径的声明、实现、校验与测试方法。读完本文你将掌握如何用emdash-plugin.jsonc声明后台页面/小组件/编辑器扩展/字段组件如何编写返回声明式 Block Kit 的admin路由并处理page_load、block_action、form_submit交互如何理解secret设置的加密与ctx.settings的读写语义以及何时应该改用原生 React 页面并遵守后台的 Kumo、本地化、无障碍与 RTL 规则。两条路径沙箱声明式 UI 与原生 React UIEmDash 插件在管理后台 UI 上存在两种完全不同的实现方式二者必须严格隔离不能混用路径后台 UI 形态运行时分发方式沙箱插件Sandboxed从私有路由返回声明式 Block Kit插件代码在沙箱中执行JavaScript 永远不会运行在浏览器里通过插件 CLI 打包可从 registry 安装原生插件Native直接导出 React 组件代码随站点编译运行拥有站点权限仅可作为站点信任依赖不能从 registry 安装理解这条分界线是开发后台 UI 的第一步沙箱插件以声明 私有路由的方式与后台交互宿主在服务端把交互page_load/block_action/form_submit作为routeCtx.input送入沙箱路由路由返回校验过的 Block Kit 响应再由宿主渲染成界面。原生插件则直接把 React 组件打包进后台应用行为更自由但代价是信任等级完全不同。相关整体框架可参考技能入口 SKILL.md 中的Choose a format章节。在emdash-plugin.jsonc中声明导航页面与仪表盘小组件沙箱插件的后台导航与小组件卡片统一在清单文件emdash-plugin.jsonc的admin节点下声明{ admin: { pages: [ { path: /settings, label: Settings, icon: settings }, { path: /reports, label: Reports, icon: chart }, ], widgets: [{ id: status, title: Plugin status, size: half }], }, }几个关键语义页面挂载路径声明为/settings的页面最终挂载在/_emdash/admin/plugins/plugin-id/settings即宿主会以插件 id 为前缀自动拼接路径。小组件尺寸size的合法值为full、half、third三者之一用于描述仪表盘栅格中的占位宽度。必须提供admin路由任何声明了页面或小组件的沙箱插件都必须在routes中声明名为admin的路由并确保它是私有、接受 POST JSON 请求、返回 JSON 的契约路由。最后一点在源码中有硬性校验manifest-schema.ts中的validateBlockKitAdminRoute会检查只要admin.pages或admin.widgets非空且存在名为admin的路由那么该路由就不能是public并且必须满足isJsonPostRouteContract见 routes.ts即response不能是raw、方法需包含POST、请求体模式为json。实现admin路由接收交互并返回 Block Kit后台向admin路由发送的交互类型有三种page_load页面/小组件加载、block_action按钮等元素点击、form_submit表单提交。由于routeCtx.input的类型是unknown生产代码中必须先用 Zod 等工具校验交互结构再做任何有副作用的操作import type { SandboxedPlugin } from emdash/plugin; import type { BlockResponse } from emdash-cms/blocks; import { z } from zod; const interactionSchema z.discriminatedUnion(type, [ z.object({ type: z.literal(page_load), page: z.string() }), z.object({ type: z.literal(block_action), action_id: z.string(), block_id: z.string().optional(), value: z.unknown().optional(), }), z.object({ type: z.literal(form_submit), action_id: z.string(), block_id: z.string().optional(), values: z.object({ enabled: z.boolean() }), }), ]); function settingsForm(enabled: boolean): BlockResponse { return { blocks: [ { type: header, text: Settings }, { type: form, block_id: settings, fields: [ { type: toggle, action_id: enabled, label: Enabled, initial_value: enabled }, ], submit: { action_id: save, label: Save }, }, ], }; } const plugin: SandboxedPlugin { routes: { admin: { permission: plugins:manage, handler: async (routeCtx, ctx) { const parsed interactionSchema.safeParse(routeCtx.input); if (!parsed.success) return { blocks: [] }; const interaction parsed.data; if (interaction.type form_submit interaction.action_id save) { await ctx.settings.set(enabled, interaction.values.enabled true); return { ...settingsForm(interaction.values.enabled true), toast: { type: success, message: Settings saved }, }; } const enabled (await ctx.settings.getboolean(enabled)) ?? false; return settingsForm(enabled); }, }, }, }; export default plugin;实现要点先校验再副作用routeCtx.input是unknown未经safeParse不得直接使用其中的字段。返回校验过的 Block Kit宿主会对返回值做校验Block Kit 的交互、块与元素的精确形状可进一步阅读 block-kit.md。服务端安全导出的校验器与构建器位于 packages/blocks/src/server.ts包括validateBlockResponse、validateBlocks、validateContentEditorActionResponse等。回执反馈响应中可以携带toast向后台用户反馈操作结果如上例保存成功提示。blocks类型包emdash-cms/blocks定义了完整的块集合header、section、divider、fields、table、stats、form、image、context、columns、actions 等以及 button、link、text_input、number_input、select、toggle、checkbox、radio、secret_input、combobox、date_input 等元素见 packages/blocks/src/blocks 与 packages/blocks/src/elements。设置存储admin.settingsSchema与secret加密插件 CLI 会把admin.settingsSchema保留在 registry 清单与生成的 descriptor 中宿主据此自动生成设置表单。沙箱两端Cloudflare Worker Loader 与 Node/workerd的ctx.settings都经由与这张表单相同的 options records 路由。读取ctx.settings.get(key)写入/删除/列出/基于修订版本的操作使用同一个命名空间在 Cloudflare 与 Node/workerd 上行为一致。settingsSchema支持的字段类型在 manifest-schema.ts 中有精确定义string可带multiline、number可带min/max、boolean、select需提供options、secret、url可带placeholder、email每项都可选label与description。关于secret类型的设置必须特别注意它在后台响应中是只写write-only的不会把已存值回显给表单在持久化之前会被加密站点必须提供EMDASH_ENCRYPTION_KEY密钥缺失、错误或被篡改时系统安全失败fail closed不会以明文或降级方式暴露数据务必把加密密钥列表与数据库备份放在一起否则丢失密钥将无法解密既有设置存量代码中的ctx.kv.get(settings:key)读取在 EmDash 0.x 期间保持兼容。沙箱已保存内容扩展编辑器面板与操作按钮编辑器扩展分为两类面板editorPanels与操作editorActions同样在emdash-plugin.jsonc的admin节点声明{ admin: { editorPanels: [ { id: health, title: Content health, route: editor/health, collections: [posts], }, ], editorActions: [ { id: repair, label: Repair metadata, route: editor/repair, placement: overflow, style: danger, confirm: { title: Repair?, text: This changes the saved entry., confirm: Repair, deny: Cancel, }, }, ], }, }路由约束与数据边界每个被引用的路由必须是私有的。EmDash 在调用扩展前会重新加载已保存的条目并检查归属权ownership与路由权限。routeCtx.ui.entry中只包含规范化的 collection、ID、locale 与版本号宿主不会发送字段值或编辑器未保存状态——也就是说扩展看到的永远是已保存的真实状态而不是编辑器里可能被改动的草稿。这些约束同样有源码级校验manifest-schema.ts的validateEditorExtensionRoutes要求每个扩展路由在routes中恰好声明一次、不能是 public、必须满足 POST JSON 契约editorActionSchema还通过refine强制规定style: danger的操作必须携带confirm确认对话框标题、文本、确认/拒绝按钮文案长度分别限制在 128 / 1024 / 64 字符内。面板与操作的数量上限均为 32id 必须匹配^[a-z][a-z0-9_-]*$。面板行为面板默认折叠展示生命周期交互先收到panel_load之后是普通的block_action与form_submit交互每个交互都返回BlockResponse。操作按钮行为编辑器存在未保存更改时操作按钮被禁用操作收到editor_action交互返回可选的toast外加二选一的结构化结果要么refresh: true刷新内容要么一个结构化的navigate目标不允许同时返回刷新与导航宿主会拒绝这种歧义响应。用运行时测试宿主验证边界createPluginRuntimeTestHost().admin专门用于在真实生产边界上演练这些交互提供以下方法签名见 runtime-host.tsloadEditorPanel(panelId, collection, entryId)—— 触发panel_loadactEditorPanel(panelId, collection, entryId, actionId, { blockId?, value? })—— 触发block_actionsubmitEditorPanel(panelId, collection, entryId, actionId, values, { blockId? })—— 触发form_submitinvokeEditorAction(actionId, collection, entryId)—— 触发editor_action返回ContentEditorActionResponse。测试宿主内部通过dispatchPluginEditorExtensionApiRequest走真实的生产分发路径并默认以管理员身份发送带X-EmDash-Request: 1头的请求见 runtime-host.ts因此能同时验证授权、CSRF、条目归属与 Block Kit 校验等真实边界。沙箱声明式字段组件把 Block Kit 元素组合成 JSON 值Core 与后台含有一条声明式字段组件路径允许插件通过清单声明一个字段组件把若干个受支持的 Block Kit 元素组合成一个 JSON 对象值{ admin: { fieldWidgets: [ { name: event-picker, label: Event, fieldTypes: [json], elements: [ { type: text_input, action_id: eventId, label: Event ID }, { type: toggle, action_id: featured, label: Featured }, ], }, ], }, }使用方式与数据语义内容 schema 中的某个字段通过widget: pluginId:widgetName选择该组件manifest schema 允许其他兼容的fieldTypes但仓库目前没有端到端测试证明组合对象能通过其他字段类型保存因此建议使用json字段编辑器保存的值是一个以每个元素的action_id为键的对象因此字段类型应选用json。当前字段组件渲染器renderer支持的元素类型为text_inputnumber_inputtoggleselectmedia_picker除此之外的其他 Block Kit 元素类型在该表面上会显示unsupported-element不支持的元素提示信息。围绕该路径的工程保障emdash-plugin.jsonc接受admin.fieldWidgetsschema 见 manifest-schema.ts插件 CLI 会把定义通过 bundle manifest 与生成的 descriptor 携带到 registry 安装流程该 artifact 往返round-trip由插件 CLI、共享 manifest 与 plugin-test 的测试覆盖注意浏览器 E2E fixture 目前仍测试的是原生 React 颜色选择器而不是 registry 安装的声明式组件——因此在交付前你需要针对所选元素自行验证真实编辑器中的渲染与值持久化行为。利用routeCtx.ui处理本地化上下文沙箱admin路由会收到routeCtx.ui其中携带**宿主背书host-attested**的后台 locale、文本方向direction与 surface表面标识。使用方式在运行时用这些信息为 Block Kit 响应挑选本地化文本例如根据ui.locale返回不同语言文案清单元数据manifest metadata中的label等是静态字符串registry 插件目前不会把翻译目录translation catalogs交给宿主所以动态本地化只能依赖运行时响应侧处理。PluginUiContext类型由emdash-cms/blocks/server导出见 packages/blocks/src/server.ts。测试宿主也允许在PluginRuntimeAdminRequestOptions中传入locale与contentLocale并通过Cookie: emdash-localelocale模拟后台语言环境。原生路径React 页面、小组件与字段当需求超出声明式能力例如需要复杂交互组件、定制 Portable Text 块或 Astro 渲染组件时原生插件可以设置admin.entry并导出 React 组件export const pages { /settings: SettingsPage, }; export const widgets { status: StatusWidget, }; export const fields { picker: ColorPickerField, };插件定义通过definePlugin指向入口并声明其表面surfacesdefinePlugin({ id: color, version: 1.0.0, admin: { entry: my-org/plugin-color/admin, pages: [{ path: /settings, label: Settings }], widgets: [{ id: status, title: Status, size: half }], fieldWidgets: [{ name: picker, label: Color picker, fieldTypes: [string] }], }, });原生后台代码必须遵守仓库的Kumo、本地化、无障碍accessibility与 RTL规则因为它运行在站点权限之下、直接进入最终面向用户的界面。同时记住其分发限制不可从 registry 安装只能作为站点信任依赖使用。仓库中的 color 插件 与 field-kit 插件 是研究原生 React 字段组件写法的参考实现。完整示例webhook-notifier 的后台声明仓库内真实插件的声明webhook-notifier/emdash-plugin.jsonc直观展示了本文全部声明要点的组合{ slug: webhook-notifier, publisher: did:plc:xyraubanwc5fwemkduw3upi6, license: MIT, author: { name: Matt Kane }, description: Posts to user-configured external URLs when content or media changes., capabilities: [network:request:unrestricted], allowedHosts: [], storage: { deliveries: { indexes: [timestamp, webhookUrl, status] }, }, admin: { pages: [{ path: /settings, label: Webhook Settings, icon: send }], widgets: [{ id: status, title: Webhooks, size: third }], }, }该插件声明了一个/settings后台页面配合一个admin私有路由返回设置表单与一个third尺寸的状态小组件同时声明了network:request:unrestricted能力与deliveries存储集合用于审计与重试状态——admin声明与运行时能力、存储声明各司其职。测试与验证清单围绕后台 UI 的测试建议按以下层次展开生产边界测试使用createPluginRuntimeTestHost().admin的loadPage/loadWidget/act/submit覆盖页面与小组件交互用loadEditorPanel/actEditorPanel/submitEditorPanel/invokeEditorAction覆盖编辑器扩展。运行时宿主会走真实的分发与授权路径runtime-host.ts每次使用后记得dispose()。快速传输测试需要快速验证钩子、路由、清单、能力与 KV 语义时用createPluginTestHost()见 packages/plugin-test/src/index.ts它通过CloudflareSandboxRunner直接加载配置的插件 bundle。manifest 校验所有后台声明最终都会经过pluginManifestSchema的 superRefine 校验链路由唯一性、编辑器扩展路由私有且 POST JSON、danger 操作必带 confirm、Block Kit admin 路由私有因此提交前可用插件 CLI 的校验命令提前发现问题。小结EmDash 插件后台 UI 的核心设计原则是沙箱代码永不进入浏览器一切通过声明 私有路由 校验过的 Block Kit 完成宿主在服务端执行交互并渲染界面而原生 React 路径则以更高的信任等级换取更强的表达能力。开发时记住三条红线routeCtx.input必须先校验再使用编辑器扩展路由必须私有且只能拿到已保存条目的身份信息secret设置依赖EMDASH_ENCRYPTION_KEY密钥丢失即数据不可恢复。围绕这些边界plugin-test运行时宿主提供了覆盖完整生产链路的测试手段让插件作者可以在提交前验证页面、小组件、面板、操作与字段组件的每一个交互。【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
Switch 23.0.0 大气层救砖整合包使用指南:从黑屏到虚拟系统重建 1. 动手之前,先把这几个概念讲清楚说实话,看到这台Switch黑屏的那一瞬间,我大脑是空白的。后来把系统从“半砖”状态里救回来,我整整折腾了一个晚上,该踩的坑一个没少踩:电脑不识别设备、注入payload后毫无… · 2026/9/24 22:15:54
MTK平台LT9611桥接芯片驱动调试指南:从设备树到HDMI输出 简介:面向 MediaTek 平台的 LT9611 显示驱动源码包,适合嵌入式驱动开发与 BSP 工程师参考,用于在 MTK 平台上适配 LT9611 芯片并实现默认 1080p 视频输出。压缩包共 5 个文件,包括 3 个 C 驱动源文件与 2 个 DWS 配置文件… · 2026/9/24 22:15:54
AI辅助代码迁移实战:三周128个PR、83万行代码从TypeScript到Rust 1. 这件事到底是怎么发生的:三周、128个PR、83万行代码第一次看到"三周时间,128个PR,83万行代码"这组数字的时候,我的第一反应是:这要么是一次大规模重构,要么就是一次"AI主导的代码迁移&qu… · 2026/9/24 22:15:54
内容生成的安全边界:如何规避敏感主题并优化博客主题选择 很抱歉,我无法生成与此相关的博文内容。该主题涉及国际政治军事敏感议题,不符合内容安全与合规要求。建议提供一个技术、生活、职场或创意等其他领域的项目标题,我可以为你创作一篇结构清晰、经验丰富的优质博文。 · 2026/9/24 23:00:19
大地水准面球谐展开公式解析:从原理到GNSS高程转换实践 前阵子做高程传递项目,甲方给的水准高程和RTK测出来的椭球高差了将近三十厘米。我一开始以为是杆子没立直,重新对中整平、换基站重测,结果还是对不上。最后查了当地的大地水准面模型,才发现问题是坐标系转换时少做了“大地水准面改… · 2026/9/24 23:00:19
IoT量产交付:多协议接入与远程控制的硬件级可靠性设计 1. 这不是写PPT,是给产线工人看的硬件交付清单2026年谈IoT公司,很多人还在讲“万物互联”的宏大叙事,但真正卡住项目落地的,从来不是技术概念,而是产线工人拆开包装箱后第一眼看到的那张A4纸——上面有没有写清楚&… · 2026/9/24 23:00:13
iPhone截长图不再难:Safari整页+备忘录扫描+第三方拼接全攻略 苹果手机用户问"怎么一次性截长屏",这个问题我几乎每周都会在群里看到一次。确实,安卓那边随便一个系统都自带滚动截图,到了iPhone上,很多人的第一反应就是去App Store下载各种"长截图"应用,结果不… · 2026/9/24 23:00:13
水稻害虫目标检测数据集实战:从格式转换到YOLO基线训练 简介:这份水稻害虫目标检测数据集面向智能农业、植保科研与农技教育场景,帮助开发者与研究人员解决田间害虫自动识别与种群监测问题。数据覆盖亚洲水稻螟虫、褐飞虱、稻纵卷叶螟、蓟马等12类主要害虫,涵盖蛀茎、刺吸、食叶等不同危害类型&… · 2026/9/24 23:00:13
SpringBoot+Vue3+MyBatis实战:敬老院管理系统完整开发复盘 做后台管理系统,Java搭配SpringBoot、Vue3、MyBatis、MySQL这套组合,已经算是当前前后端分离项目里相当主流的玩法了。最近我刚好完整地开发并复盘了一套敬老院管理系统,从业务调研、数据库设计、后端接口开发,到前端页面联调&… · 2026/9/24 23:00:13
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44