- CMS
- 后端
- 前端
- 插件系统
【免费下载链接】emdash
EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress
EmDash 的 seed 文件(seed/seed.json)是一个数据库无关的 JSON 文档,它一次性声明了站点的全部 schema——集合(collections)、字段(fields)、分类法(taxonomies)、菜单(menus)、组件区(widget areas)、设置(settings)以及可选的示例内容(content)。这份文件在构建时被内联进产物,当数据库为空且设置向导尚未完成时,会在首次请求时自动应用。阅读本文后,你将掌握 seed 文件的完整结构、每种配置项的写法与取值规则,以及如何通过emdash export-seed命令从现有数据库反向导出 seed,从而具备"从零建站"与"站点迁移"的完整能力。
Seed 文件整体结构
一个 seed 文件以$schema与version开头,随后按功能分区组织站点定义。下面的骨架对应 types.ts 中SeedFile接口的全部顶层字段:
{ "$schema": "https://emdashcms.com/seed.schema.json", "version": "1", "meta": { "name": "My Site", "description": "A description of this site", "author": "Author Name" }, "settings": { ... }, "collections": [ ... ], "taxonomies": [ ... ], "menus": [ ... ], "widgetAreas": [ ... ], "sections": [ ... ], "bylines": [ ... ], "content": { ... } }各字段的语义如下:
$schema:JSON Schema 引用,用于编辑器校验提示;version:seed 格式版本,当前必须为"1"(validate.ts 会拒绝其他版本);meta:种子元信息,仅作描述,不参与建库;settings:站点级设置(标题、标语等);collections:内容类型定义,是 seed 的核心;taxonomies:分类/标签体系;menus:导航菜单;widgetAreas:可配置组件区域;sections:可在编辑器中通过/section命令插入的可复用内容块;bylines:署名作者档案(独立于用户账号);content:按集合 slug 组织的示例内容;- 此外
SeedFile还支持redirects(重定向规则)与defaultLocale(多语言场景下省略locale的行所回退的默认语言,见 types.ts)。
Collections:定义内容类型
集合定义了站点的内容类型。每个集合在数据库中对应一张名为ec_{slug}的表,这一点可以从 apply.ts 中通过SchemaRegistry批量建表、以及 export-seed.ts 中直接查询ec_${collection.slug}表得到印证。
{ "slug": "posts", "label": "Posts", "labelSingular": "Post", "supports": ["drafts", "revisions", "search", "seo"], "commentsEnabled": true, "fields": [ ... ] }Collection Supports
supports数组声明集合开启哪些平台能力:
| Support | Description |
|---|---|
drafts | Draft/published workflow |
revisions | Revision history |
search | Full-text search indexing |
seo | SEO meta fields in admin |
根据 types.ts 的类型定义,supports还支持preview(预览)与scheduling(定时发布)。在 apply.ts 中可以看到search支持的实际效果:应用 seed 后,引擎会对含可搜索字段的集合自动调用ftsManager.enableSearch()开启全文搜索。
Slug 规则
集合与字段 slug 必须满足:
- 小写字母数字加下划线:
/^[a-z][a-z0-9_]*$/ - 最大 63 个字符
- 不能与保留 slug 冲突
该正则定义在 validate.ts 中,同时会检查重复的集合 slug。另外集合字段的 slug 校验也使用同一模式。
更多集合级选项
除原文档示例外,types.ts 还定义了大量可选项,全部可由 apply.ts 中的建集合逻辑处理:
urlPattern:集合条目的 URL 模式;routable(默认true):是否要求条目在发布前具备 slug;hidden:是否从管理后台侧边栏与快捷操作中隐藏(仍可通过 API、MCP、插件钩子访问);sortOrder:管理后台侧边栏的显式排序(升序,未设置的集合保持字母序并排在有序集合之后);group:与其他集合共享的管理后台侧边栏文件夹;commentsEnabled:是否启用评论;editLocking(默认true):打开条目时是否获取编辑锁;titleField/dateField:指定驱动管理后台列表标题列与日期列的字段 slug(见 apply.ts,二者会在字段创建成功后单独回写校验)。
Field Types:字段类型与数据库列映射
每种字段类型对应一种数据库列类型与一种运行时形态:
| Type | Column type | Runtime shape | Notes |
|---|---|---|---|
string | TEXT | string | Single line text |
text | TEXT | string | Multi-line text (textarea) |
number | REAL | number | Floating point |
integer | INTEGER | number | Whole numbers |
boolean | INTEGER | boolean | Stored as 0/1 |
datetime | TEXT | Date | ISO 8601 string in DB |
image | TEXT | { id, src?, alt?, width?, height? } | Object, not a string |
reference | TEXT | string(ID) | Reference to another entry |
portableText | JSON | PortableTextBlock[] | Rich text as structured JSON |
json | JSON | any | Arbitrary JSON data |
注意image字段在运行时是对象而非字符串(包含id等元数据),boolean在数据库中存为 0/1,datetime以 ISO 8601 字符串存储。
Field Definition
{ "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true }字段可用的属性(SeedField,见 types.ts):
slug(必填)——字段标识符;label(必填)——管理后台显示名;type(必填)——上述类型之一;required——校验必填;searchable——纳入全文搜索索引;unique——字段值唯一;indexed——为字段建索引;defaultValue——默认值;validation——自定义校验规则;widget——指定管理后台使用的输入组件;options——传给 widget/字段的附加选项(如reference字段可指定目标集合)。
常见字段组合模式
博客文章(Blog post):
"fields": [ { "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true }, { "slug": "featured_image", "label": "Featured Image", "type": "image" }, { "slug": "content", "label": "Content", "type": "portableText", "searchable": true }, { "slug": "excerpt", "label": "Excerpt", "type": "text" } ]作品集项目(Portfolio project):
"fields": [ { "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true }, { "slug": "featured_image", "label": "Featured Image", "type": "image", "required": true }, { "slug": "client", "label": "Client", "type": "string" }, { "slug": "year", "label": "Year", "type": "string" }, { "slug": "summary", "label": "Summary", "type": "text", "searchable": true }, { "slug": "content", "label": "Content", "type": "portableText", "searchable": true }, { "slug": "gallery", "label": "Gallery", "type": "json" }, { "slug": "url", "label": "Project URL", "type": "string" } ]页面(Page,极简):
"fields": [ { "slug": "title", "label": "Title", "type": "string", "required": true, "searchable": true }, { "slug": "content", "label": "Content", "type": "portableText", "searchable": true } ]Taxonomies:分类与标签体系
Taxonomies 是挂在集合上的标签/分类系统。hierarchical: true表示树形结构(类似 WordPress 的分类),hierarchical: false则是扁平列表(类似 WordPress 的标签)。collections声明该分类法应用于哪些集合,terms定义预置的分类项。
{ "name": "category", "label": "Categories", "labelSingular": "Category", "hierarchical": true, "collections": ["posts"], "terms": [ { "slug": "development", "label": "Development" }, { "slug": "design", "label": "Design" } ] }hierarchical: true—— 树形结构(类似 WordPress categories)hierarchical: false—— 扁平列表(类似 WordPress tags)collections—— 该分类法适用的集合terms—— 预创建的分类项
在 apply.ts 中,层级分类项通过applyHierarchicalTerms多轮次应用:每轮只处理父项已就绪的 term,最多重试 10 轮,以支持深层嵌套;每个分类项还可携带parent(父项 slug)与可选的translationOf引用。注意 terms 属于"示例数据"范畴,仅在includeContent为 true 时才会创建(SeedApplyOptions.includeContent,见 types.ts)。
Menus:导航菜单
菜单由管理后台维护,seed 中提供初始结构。type: "custom"的菜单项使用任意 URL;内容引用类菜单项(页面/文章)在渲染时解析。
{ "name": "primary", "label": "Primary Navigation", "items": [ { "type": "custom", "label": "Home", "url": "/" }, { "type": "custom", "label": "About", "url": "/pages/about" }, { "type": "custom", "label": "Posts", "url": "/posts" } ] }菜单项类型:
custom—— 任意 URL
从 types.ts 可见菜单项还支持更多字段:非custom项使用ref(内容 id 或分类项 slug)配合collection(目标集合名)引用条目;可选target(_blank/_self)、titleAttr、cssClasses,以及嵌套children构建多级菜单。apply.ts 会按sort_order递归构建菜单树;export-seed.ts 中的buildMenuItemTree展示了相同的父子关系处理逻辑。菜单在内容之后应用,这样菜单中的内容引用($ref)可以解析到已创建的条目。
Widget Areas:可配置组件区域
组件区是编辑者可以放置可配置组件的命名区域。
{ "name": "sidebar", "label": "Sidebar", "description": "Widget area displayed on single post pages", "widgets": [ { "type": "component", "componentId": "core:search", "title": "Search" }, { "type": "component", "componentId": "core:categories", "title": "Categories" }, { "type": "component", "componentId": "core:tags", "title": "Tags" }, { "type": "component", "componentId": "core:recent-posts", "title": "Recent Posts", "settings": { "count": 5, "showDate": true } }, { "type": "component", "componentId": "core:archives", "title": "Archives", "settings": { "type": "monthly", "limit": 6 } }, { "type": "content", "title": "About", "content": [ { "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Some rich text content." }] } ] } ] }Widget 类型
| Type | Description | Key fields |
|---|---|---|
content | Rich text (Portable Text) | content |
menu | Navigation menu | menuName |
component | Core or custom component | componentId,settings |
根据 types.ts,component组件的配置在类型定义中为props(导出代码 export-seed.ts 中也是读写props字段);isWidgetType严格限定三种合法类型。应用时组件区会"整体重建"——已存在的区域先清空旧 widgets 再写入新 widgets(apply.ts)。
核心组件
core:search—— 搜索表单core:categories—— 带计数的分类列表core:tags—— 标签云core:recent-posts—— 最新文章列表core:archives—— 按月归档链接
Sections:可复用内容块
Sections 是可复用内容块,编辑者在编辑器中通过/section斜杠命令插入。
{ "slug": "newsletter-signup", "title": "Newsletter Signup", "description": "A call-to-action block for newsletter subscriptions", "keywords": ["newsletter", "subscribe", "email", "cta"], "source": "theme", "content": [ { "_type": "block", "style": "h3", "children": [{ "_type": "span", "text": "Stay in the loop" }] }, { "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "Get notified when new posts are published." }] } ] }types.ts 中source取值为"theme"(seed 提供)或"import"(WordPress 导入);theme_id在应用时随source写入。应用逻辑见 apply.ts。
Bylines:署名作者档案
Bylines 是独立的署名作者档案,与用户账号无关。seed 中声明的 byline 通过id被内容条目的bylines数组引用。
{ "id": "byline-editorial", "slug": "emdash-editorial", "displayName": "EmDash Editorial" }客座作者:
{ "id": "byline-guest", "slug": "guest-contributor", "displayName": "Guest Contributor", "isGuest": true }types.ts 中SeedByline还支持bio、websiteUrl以及avatar(通过已存储文件的storageKey关联头像,不会触发下载,适合配合媒体迁移一起 seed)。byline 属于示例数据,同样只在includeContent为 true 时应用(apply.ts)。
Settings:站点设置
"settings": { "title": "My Blog", "tagline": "Thoughts on building for the web" }可用键:title、tagline、logo、favicon、social、timezone、dateFormat。设置以site:前缀存入选项表(见 apply.ts 与 export-seed.ts 中SETTINGS_PREFIX的读写对称逻辑)。onConflict对设置按 key 逐个处理:"skip"只创建缺失 key,"update"覆盖传入 key,"error"遇到首个已存在 key 即报错。
Content:示例内容
内容按集合 slug 组织。每个条目包含id(seed 内唯一标识,用于$ref解析)、slug、status(published/draft)、data(字段 slug → 值),以及可选的bylines与taxonomies关联:
"content": { "posts": [ { "id": "post-1", "slug": "hello-world", "status": "published", "data": { "title": "Hello World", "excerpt": "My first post.", "featured_image": { "$media": { "url": "https://images.unsplash.com/photo-xxx?w=1200&h=800&fit=crop", "alt": "Description of image", "filename": "hello-world.jpg" } }, "content": [ { "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "This is the body text." }] } ] }, "bylines": [ { "byline": "byline-editorial" } ], "taxonomies": { "category": ["development"], "tag": ["webdev", "opinion"] } } ], "pages": [ { "id": "about", "slug": "about", "status": "published", "data": { "title": "About", "content": [ { "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "About this site." }] } ] } } ] }Content 中的媒体引用:$media
图片字段使用$media语法,EmDash 会下载并存储该图片(下载 → 上传到配置的存储 → 创建媒体记录 → 替换为正式字段值,这一流程注释在 types.ts 中):
"featured_image": { "$media": { "url": "https://images.unsplash.com/photo-xxx?w=1200&h=800&fit=crop", "alt": "Description", "filename": "my-image.jpg" } }如需使用外部图片而不下载,直接给字符串 URL:
"featured_image": "https://images.unsplash.com/photo-xxx?w=1200"SeedApplyOptions还提供skipMediaDownload(将$media解析为使用原始外部 URL 的externalprovider 媒体值,无需存储适配器,适合 playground/演示环境)与mediaBasePath(本地媒体文件的基础路径),见 types.ts。
Content 中的引用字段:$ref
使用$ref:id格式引用其他条目:
"author": "$ref:byline-editorial"applySeed在应用过程中维护 seed id → 真实条目 id 的映射表(seedIdMap),先创建被引用目标、后创建引用方,从而解析$ref。导出侧(export-seed.ts)则通过orderByReferenceTargets对集合做依赖排序,保证引用目标先被写入。
Content 中的 Portable Text
portableText类型的内容字段是 block 数组:
[ { "_type": "block", "style": "normal", "children": [{ "_type": "span", "text": "A paragraph." }] }, { "_type": "block", "style": "h2", "children": [{ "_type": "span", "text": "A heading" }] }, { "_type": "block", "style": "blockquote", "children": [{ "_type": "span", "text": "A quote." }] } ]行内标记(加粗、斜体、链接):
{ "_type": "block", "style": "normal", "children": [ { "_type": "span", "text": "This is " }, { "_type": "span", "text": "bold", "marks": ["strong"] }, { "_type": "span", "text": " and " }, { "_type": "span", "text": "italic", "marks": ["em"] } ] }块级样式支持:normal、h1-h6、blockquote。
草稿内容
设置"status": "draft"创建未发布内容:
{ "id": "post-draft", "slug": "work-in-progress", "status": "draft", "data": { ... } }从 apply.ts 的实现细节看,status: "published"的条目在创建后会被立即提升为 live revision(填充live_revision_id,管理后台显示"Unpublish"而非"Save & Publish"),status: "draft"则保持草稿。
真实示例:marketing-cloudflare 模板的 seed
仓库中 templates/marketing-cloudflare/seed/seed.json 是一个完整可运行的 seed:它定义了一个pages集合(含title与content字段)、四个导航菜单(primary以及三个页脚菜单footer_product/footer_company/footer_support),并在content.pages中预置了home、pricing、contact三个页面。其正文大量使用marketing.hero、marketing.features、marketing.testimonials、marketing.pricing、marketing.faq等自定义 block 类型,与 src/components/blocks 下的组件一一对应——这展示了 seed 内容与主题组件如何通过_type完成绑定,是编写带插件内容 seed 的绝佳参照。
应用 Seeds:时机、位置与幂等性
seed 的加载入口在 load.ts:它通过 Vite 的虚拟模块virtual:emdash/seed在构建时将用户 seed(或默认 seed)内联进产物,从而避免运行时文件系统访问(在 workerd/miniflare 中process.cwd()返回/)。
seed 文件可以放在以下任一位置(按优先级):
.emdash/seed.jsonpackage.json#emdash.seedseed/seed.json
它被内联进构建,在数据库为空且设置向导未完成时于首次请求自动应用。已有数据永远不会被覆盖——applySeed是幂等的,可安全重复执行。
应用顺序
applySeed(apply.ts)严格按照依赖顺序写入,先决条件是外键与引用都能解析:
- Site settings
- Collections + Fields
- Taxonomy definitions + Terms
- Bylines
- Content(先于菜单,使菜单引用可解析)
- Menus + Menu items(此时可解析内容引用)
- Redirects
- Widget areas + Widgets
- Sections
- 对
supports含search的集合启用全文搜索
冲突处理
SeedApplyOptions.onConflict决定冲突时的行为(默认"skip"):"skip"跳过已存在项、"update"覆盖、"error"在首个冲突处抛错。另需注意includeContent默认false——即默认只应用 schema 与结构(集合、字段、分类法定义、菜单、设置、重定向、组件区、sections),内容条目、byline 与分类项属于示例数据,需显式开启。
验证:何时失败
校验在应用时运行(validateSeed,validate.ts)。常见错误包括:
- 图片字段使用原始 URL(应使用
$media) - 引用字段使用原始 ID(应使用
$ref:id) - PortableText 不是数组或缺少
_type - 类型不匹配(string 与 number 混用等)
validateSeed还会检查:version必须为"1";集合 slug 必须匹配/^[a-z][a-z0-9_]*$/且不重复;defaultLocale必须是非空且无首尾空格的字符串;重定向路径必须以/开头、不能含//开头、CRLF 或..路径穿越(validate.ts)。
如果 seed 无效,首次请求会失败并记录错误。修复后需重启开发服务器。
导出 Seeds:从数据库反向生成
通过 CLI 可以从现有数据库导出 seed 文件,用于站点备份、克隆或模板化:
npx emdash export-seed # Schema only npx emdash export-seed --with-content # Schema + all content npx emdash export-seed --with-content=posts,pages # Specific collections命令定义在 export-seed.ts,支持的完整参数:
--database, -d:数据库路径,默认./data.db;--cwd:工作目录,默认当前进程目录;--with-content:包含内容(all与裸标志、true等价,或逗号分隔的集合名列表);--pretty:JSON 是否美化输出,默认true。
导出过程有以下实现要点:
- 导出前会检查迁移状态:存在待执行迁移或由更新版本 EmDash 迁移过的数据库会直接报错(export-seed.ts);
- 命令以只读方式连接数据库;
- seed 文档写入stdout(便于
emdash export-seed > seed.json重定向),诊断信息写入 stderr,避免污染输出; - 导出内容时,图片字段被转换回
$media语法、引用字段被转换回$ref:seedId形式(processDataForExport,export-seed.ts); - 内容条目的 seed id 形如
collectionSlug:itemSlug(多语言下追加:locale),保证$ref在往返(export → seed)后依然稳定; - 导出会检测数据中的语言分布:多个 locale 时输出带
locale与translationOf的多语言结构,单个非enlocale 时通过顶层defaultLocale自描述,避免往返时被回填成en(见 export-seed.ts 中的detectLocaleInfo)。
小结
seed 文件是 EmDash 建站与迁移的枢纽:它把"内容类型的数据库表结构、后台配置、导航、组件区、示例内容"压缩成一份声明式 JSON,构建期内联、首请求自动应用、重复执行幂等;配合emdash export-seed又能在任意时刻把线上数据库的 schema 与内容完整导出。对开发者和模板作者来说,掌握collections/fields的字段类型映射、$media/$ref/Portable Text 的书写规范,以及onConflict/includeContent等应用选项,就能像写配置文件一样搭建出结构完整、可直接运营的 EmDash 站点。上述所有行为均可在 packages/core/src/seed 目录下找到对应实现与测试佐证。
- CMS
- 后端
- 前端
- 插件系统
【免费下载链接】emdash
EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress
相关推荐
EmDash Schema 与 Seed 文件完全指南:用 seed.json 定义内容模型与示例数据
EmDash Schema 与 Seed 文件完全指南:用 seed.json 定义内容模型与示例数据 导读 :本文基于 EmDash 官方技能文档 schem
CMS后端前端插件系统Thunderbolt 5 RDMA:JACCL低延迟分布式通信实战
Thunderbolt 5 RDMA:JACCL低延迟分布式通信实战 四台 M3 Ultra 张量并行推理:TCP 延迟为何成了瓶颈 四台 M3 Ultra 用
CMS后端前端插件系统CopilotKit 工具驱动式生成 UI 实战:LangGraph TypeScript Agent 中用 useComponent 把工具结果渲染为 React 图表
CopilotKit 工具驱动式生成 UI 实战:LangGraph TypeScript Agent 中用 useComponent 把工具结果渲染为 Rea
CMS后端前端插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考