EmDash Schema and Seed 文件完全指南:用 seed.json 一键定义站点结构与示例内容
2026/9/24 9:15:02 网站建设 项目流程
  • CMS
  • 后端
  • 前端
  • 插件系统

【免费下载链接】emdash

EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

EmDash 的 seed 文件(seed/seed.json)是一个数据库无关的 JSON 文档,它一次性声明了站点的全部 schema——集合(collections)、字段(fields)、分类法(taxonomies)、菜单(menus)、组件区(widget areas)、设置(settings)以及可选的示例内容(content)。这份文件在构建时被内联进产物,当数据库为空且设置向导尚未完成时,会在首次请求时自动应用。阅读本文后,你将掌握 seed 文件的完整结构、每种配置项的写法与取值规则,以及如何通过emdash export-seed命令从现有数据库反向导出 seed,从而具备"从零建站"与"站点迁移"的完整能力。

Seed 文件整体结构

一个 seed 文件以$schemaversion开头,随后按功能分区组织站点定义。下面的骨架对应 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数组声明集合开启哪些平台能力:

SupportDescription
draftsDraft/published workflow
revisionsRevision history
searchFull-text search indexing
seoSEO 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:字段类型与数据库列映射

每种字段类型对应一种数据库列类型与一种运行时形态:

TypeColumn typeRuntime shapeNotes
stringTEXTstringSingle line text
textTEXTstringMulti-line text (textarea)
numberREALnumberFloating point
integerINTEGERnumberWhole numbers
booleanINTEGERbooleanStored as 0/1
datetimeTEXTDateISO 8601 string in DB
imageTEXT{ id, src?, alt?, width?, height? }Object, not a string
referenceTEXTstring(ID)Reference to another entry
portableTextJSONPortableTextBlock[]Rich text as structured JSON
jsonJSONanyArbitrary 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)、titleAttrcssClasses,以及嵌套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 类型

TypeDescriptionKey fields
contentRich text (Portable Text)content
menuNavigation menumenuName
componentCore or custom componentcomponentId,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还支持biowebsiteUrl以及avatar(通过已存储文件的storageKey关联头像,不会触发下载,适合配合媒体迁移一起 seed)。byline 属于示例数据,同样只在includeContent为 true 时应用(apply.ts)。

Settings:站点设置

"settings": { "title": "My Blog", "tagline": "Thoughts on building for the web" }

可用键:titletaglinelogofaviconsocialtimezonedateFormat。设置以site:前缀存入选项表(见 apply.ts 与 export-seed.ts 中SETTINGS_PREFIX的读写对称逻辑)。onConflict对设置按 key 逐个处理:"skip"只创建缺失 key,"update"覆盖传入 key,"error"遇到首个已存在 key 即报错。

Content:示例内容

内容按集合 slug 组织。每个条目包含id(seed 内唯一标识,用于$ref解析)、slugstatuspublished/draft)、data(字段 slug → 值),以及可选的bylinestaxonomies关联:

"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"] } ] }

块级样式支持:normalh1-h6blockquote

草稿内容

设置"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集合(含titlecontent字段)、四个导航菜单(primary以及三个页脚菜单footer_product/footer_company/footer_support),并在content.pages中预置了homepricingcontact三个页面。其正文大量使用marketing.heromarketing.featuresmarketing.testimonialsmarketing.pricingmarketing.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.json
  • package.json#emdash.seed
  • seed/seed.json

它被内联进构建,在数据库为空且设置向导未完成时于首次请求自动应用。已有数据永远不会被覆盖——applySeed是幂等的,可安全重复执行。

应用顺序

applySeed(apply.ts)严格按照依赖顺序写入,先决条件是外键与引用都能解析:

  1. Site settings
  2. Collections + Fields
  3. Taxonomy definitions + Terms
  4. Bylines
  5. Content(先于菜单,使菜单引用可解析)
  6. Menus + Menu items(此时可解析内容引用)
  7. Redirects
  8. Widget areas + Widgets
  9. Sections
  10. supportssearch的集合启用全文搜索

冲突处理

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 时输出带localetranslationOf的多语言结构,单个非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

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询