REST API 参考

本页内容

EmDash 在 /_emdash/api/ 暴露 REST API,用于内容管理、媒体上传和 schema 操作。

身份验证

API 请求需要通过 Authorization 头中的 Bearer 令牌进行身份验证:

Authorization: Bearer <token>

通过管理界面或编程方式生成令牌。

响应格式

所有响应遵循一致的格式。成功响应将结果包裹在 data 中:

{
  "success": true,
  "data": { ... }
}

错误响应包含代码、消息和可选的详情:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message",
    "details": { ... }
  }
}

内容端点

列出内容

GET /_emdash/api/content/:collection

参数

参数类型描述
collectionstring集合 slug(路径参数)
cursorstring分页游标(查询参数)
limitnumber每页条目数(查询参数,默认:50)
statusstring按状态过滤(查询参数)
orderBystring排序字段(查询参数)
orderstring排序方向:ascdesc(查询参数)

响应

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "01HXK5MZSN...",
        "type": "posts",
        "slug": "hello-world",
        "data": { "title": "Hello World", ... },
        "status": "published",
        "createdAt": "2025-01-24T12:00:00Z",
        "updatedAt": "2025-01-24T12:00:00Z"
      }
    ],
    "nextCursor": "eyJpZCI6..."
  }
}

获取内容

GET /_emdash/api/content/:collection/:id

响应

{
  "success": true,
  "data": {
    "item": {
      "id": "01HXK5MZSN...",
      "type": "posts",
      "slug": "hello-world",
      "data": { "title": "Hello World", ... },
      "status": "published",
      "createdAt": "2025-01-24T12:00:00Z",
      "updatedAt": "2025-01-24T12:00:00Z"
    }
  }
}

创建内容

POST /_emdash/api/content/:collection
Content-Type: application/json

请求体

{
  "data": {
    "title": "New Post",
    "content": [...]
  },
  "slug": "new-post",
  "status": "draft"
}

响应

{
  "success": true,
  "data": {
    "item": { ... }
  }
}

更新内容

PUT /_emdash/api/content/:collection/:id
Content-Type: application/json

请求体

{
	"data": {
		"title": "Updated Title"
	},
	"status": "published"
}

删除内容

DELETE /_emdash/api/content/:collection/:id

响应

{
	"success": true,
	"data": {
		"success": true
	}
}

媒体端点

列出媒体

GET /_emdash/api/media?includeUsage=1

参数

参数类型描述
cursorstring不透明分页游标
pagenumber编号页面,从 1 开始;不能与 cursor 同时使用
limitnumber每页条目数,1 到 100(默认:50)
mimeTypestring按一个或多个逗号分隔的 MIME 类型过滤
qstring不区分大小写的文件名搜索
folderIdstring文件夹 ID,或 unfiled 表示主库
includeUsage1在每个返回项上包含覆盖范围感知的 usage 摘要

省略 folderId 可列出主库和所有文件夹中的媒体。使用 folderId=unfiled 仅列出未分配到文件夹的媒体。编号请求返回 totalCount;游标模式在还有下一页时返回 nextCursor

响应

{
	"success": true,
	"data": {
		"items": [
			{
				"id": "01HXK5MZSN...",
				"filename": "photo.jpg",
				"mimeType": "image/jpeg",
				"size": 102400,
				"width": 1920,
				"height": 1080,
				"folderId": null,
				"url": "/_emdash/api/media/file/uploads/photo.jpg",
				"createdAt": "2025-01-24T12:00:00Z",
				"usage": {
					"count": 3,
					"coverage": {
						"scope": "all_content_collections",
						"status": "complete"
					}
				}
			}
		],
		"nextCursor": "eyJpZCI6..."
	}
}

获取媒体

GET /_emdash/api/media/:id?includeUsage=1

includeUsage 在列表和获取端点上均为可选参数。其唯一接受的值是 1。省略时,usage 属性将被省略,服务器不会运行使用查询。

使用摘要

usage.count 是引用该媒体项的不同活跃 EmDash 内容行或语言环境的数量。对同一内容条目的重复引用和多个源变体只计算一次。已删除的内容不计入。

数字计数可能会暴露类似草稿的内容。仅当会话用户具有 content:read_drafts 权限,或当 API 令牌具有 admin 作用域且其关联用户也具有该权限时才返回。其他媒体读取者收到 usage.count: null;这是成功的脱敏响应,而非错误。

每个请求的摘要包含所有当前已注册内容集合的聚合覆盖范围:

状态含义
complete每个已注册集合都有当前的、已完成的使用覆盖
never没有已注册集合完成过初始使用修复
running使用修复正在运行中
partial覆盖范围混合,或仅部分已注册范围被索引
failed跨已注册范围的覆盖失败
stale索引覆盖已过时
unknown存储的覆盖包含此版本无法识别的状态

complete 在下述支持字段内支持作用域完全零声明。具有任何其他状态的计数是索引投影,可能高报或低报。即使是完整结果在并发写入期间也是参考性的;使用读取不是事务锁,不得用作删除保证。

获取媒体使用详情

GET /_emdash/api/media/:id/usage?limit=50&cursor=...

此端点需要 media:readcontent:read_drafts 权限。令牌认证的调用者还需要 admin 作用域;令牌作用域不会绕过关联用户的权限。

limit 控制每页的内容条目组数,从 1 到 100(默认:50)。分页永远不会分割一个返回条目组的源或出现次数。

{
	"data": {
		"items": [
			{
				"collection": "posts",
				"contentId": "01CONTENT...",
				"title": "Launch notes",
				"slug": "launch-notes",
				"locale": "en",
				"status": "published",
				"scheduledAt": null,
				"deletedAt": null,
				"sources": [
					{
						"variant": "columns",
						"occurrences": [
							{
								"fieldSlug": "hero",
								"fieldPath": "hero",
								"occurrenceIndex": 0,
								"referenceType": "image_field"
							}
						]
					}
				]
			}
		],
		"nextCursor": "eyJvcmRlclZhbHVlIjoicG9zdHMiLCJpZCI6IjAxLi4uIn0",
		"coverage": {
			"scope": "all_content_collections",
			"status": "complete"
		}
	}
}

授权详情包括活跃和已删除的条目。非空的 deletedAt 标识已删除的条目。源为 columnsdraft_overlay;出现次数标识支持的字段和路径,但不暴露内部索引元数据。

媒体使用覆盖 EmDash 内容集合中存储的顶级图片和文件字段、重复器图片字段和 Portable Text 图片块中的本地媒体引用。它不扫描自定义代码、渲染的 HTML、设置、菜单、小部件、插件私有数据、外部站点或仅供提供商使用的资源。

上传媒体

上传媒体需要 media:upload 权限。Bearer 令牌还需要 media:write 作用域。默认最大文件大小为 50 MB。设置 maxUploadSize 以更改限制。

使用以下上传方法之一:

  • 当客户端可以通过 EmDash 发送文件时,使用直接 multipart 上传。
  • 使用上传目标流程直接上传到 S3 兼容存储。本地存储和原生 R2 会返回 EmDash 上传 URL。

直接 multipart 上传

在 multipart 请求的 file 字段中发送文件:

curl --request POST \
  --header "Authorization: Bearer $EMDASH_TOKEN" \
  --header "X-EmDash-Request: 1" \
  --form "file=@./photo.jpg;type=image/jpeg" \
  https://example.com/_emdash/api/media

curl 会自动将 multipart 边界添加到 Content-Type 头。不要手动设置该头。

该端点还接受以下可选 multipart 字段:

字段描述
width图片宽度(像素)
height图片高度(像素)
fieldId其配置的 MIME 类型允许列表适用于上传的字段
thumbnail缩小的图片,用于生成低质量图片占位符

新上传返回 201 Created,包含存储的媒体项和 folderId: null。如果相同文件已存在,EmDash 返回 200 OK,包含 deduplicated: true、现有媒体项及其当前文件夹分配。直接 multipart 上传立即可用,不使用确认端点。

上传目标流程

上传目标流程在最终确认之前将媒体项保持在 pending 状态。待处理的媒体不会出现在标准媒体列表或媒体库中。

  1. 请求上传目标

    POST /_emdash/api/media/upload-url
    Authorization: Bearer <token>
    Content-Type: application/json
    
    {
    	"filename": "photo.jpg",
    	"contentType": "image/jpeg",
    	"size": 102400
    }

    filenamecontentTypesize 是必需的。请求还接受以下可选字段:

    字段描述
    contentHashsha1: 加 40 个小写十六进制字符,用于查找匹配项
    fieldId其配置的 MIME 类型允许列表适用于上传的字段

    响应包含文件上传的 URL、方法和头。当存储适配器支持时,uploadUrl 是一个绝对签名 URL。否则,它是一个根相对的 EmDash 端点。

    {
    	"success": true,
    	"data": {
    		"uploadUrl": "/_emdash/api/media/01M0AFKJS0RJM3WV69QHAY7YA1/upload",
    		"method": "PUT",
    		"headers": {
    			"Content-Type": "image/jpeg",
    			"X-EmDash-Request": "1"
    		},
    		"mediaId": "01M0AFKJS0RJM3WV69QHAY7YA1",
    		"storageKey": "01M0AFKJS0K2YF0222NP6ENYWX.jpg",
    		"expiresAt": "2026-08-18T14:05:09.920Z"
    	}
    }

    如果 contentHash 与具有相同 MIME 类型和大小的现有媒体项匹配,响应包含 existing: truemediaIdstorageKeyurl,而不是上传目标。使用返回的媒体项并停止。不要上传或确认文件。

  2. 将文件上传到 uploadUrl

    从返回的 methodheaders 开始。将相对 uploadUrl 解析为 EmDash 站点 URL。

    对于同源 EmDash 目标,在上传请求中包含 Bearer 令牌。对于另一个源上的签名 URL,仅发送返回的上传头。

    同源上传返回以下响应。签名 URL 返回存储提供商的响应。

    {
    	"success": true,
    	"data": {
    		"uploaded": true,
    		"size": 102400
    	}
    }
  3. 确认上传

    确认操作检查存储的文件并将媒体项从 pending 更改为 readysizewidthheight 是可选的,但 EmDash 在提供时会验证它们。

    POST /_emdash/api/media/01M0AFKJS0RJM3WV69QHAY7YA1/confirm
    Authorization: Bearer <token>
    Content-Type: application/json
    
    {
    	"size": 102400,
    	"width": 1920,
    	"height": 1080
    }

    响应包含就绪的媒体项:

    {
    	"success": true,
    	"data": {
    		"item": {
    			"id": "01M0AFKJS0RJM3WV69QHAY7YA1",
    			"filename": "photo.jpg",
    			"status": "ready",
    			"url": "/_emdash/api/media/file/01M0AFKJS0K2YF0222NP6ENYWX.jpg"
    		}
    	}
    }

上传错误

状态代码原因
400NO_FILEmultipart 请求没有 file 字段,或上传体缺失
400INVALID_TYPEMIME 类型不允许或与待处理媒体项不匹配
400VALIDATION_ERROR上传元数据缺失、无效或超过配置的大小限制
400FILE_NOT_FOUND确认时找不到上传的对象
400UPLOAD_SIZE_MISMATCH声明的、上传的和确认的大小不匹配
400INVALID_STATE媒体项不是待处理状态
404NOT_FOUND媒体项不存在
409INVALID_STATE待处理的媒体项在确认期间发生更改
413PAYLOAD_TOO_LARGE直接或同源上传太大

更新媒体

PUT /_emdash/api/media/:id
Content-Type: application/json

请求体

{
	"alt": "Photo description",
	"caption": "Photo caption",
	"folderId": "01FOLDER..."
}

省略 folderId 保持当前分配不变。设置为 nullunfiled 将媒体项返回到主库。分配自己拥有的媒体需要 media:edit_own;分配任何媒体需要 media:edit_any。Bearer 令牌还需要 media:write 作用域。

列出媒体文件夹

GET /_emdash/api/media/folders?limit=50&q=product&cursor=...
参数类型描述
cursorstring不透明分页游标
limitnumber每页文件夹数,1 到 100(默认:50)
qstring不区分大小写的部分文件夹名搜索(1–200 个字符)

按名称顺序返回文件夹,可选 nextCursor。该端点需要 media:read

获取媒体文件夹

GET /_emdash/api/media/folders/:id

返回请求 ID 的文件夹。该端点需要 media:read,当文件夹不存在时返回 404。

创建媒体文件夹

POST /_emdash/api/media/folders
Content-Type: application/json

{
	"name": "Product photos"
}

返回 201 Created,包含创建的文件夹:

{
	"success": true,
	"data": {
		"item": {
			"id": "01HXK5MZSN...",
			"name": "Product photos"
		}
	}
}

文件夹名称会被修剪,必须包含 1 到 200 个字符。名称在 Unicode 规范化和小写转换后进行比较,因此 PhotosphotosPHOTOS 被视为重复。 创建、重命名和删除文件夹需要 media:edit_any。Bearer 令牌还需要 media:write 作用域。

重命名媒体文件夹

PUT /_emdash/api/media/folders/:id
Content-Type: application/json

{
	"name": "Published product photos"
}

返回 200 OK,包含更新后的文件夹,响应格式与创建相同。

删除媒体文件夹

DELETE /_emdash/api/media/folders/:id

返回 200 OK,响应 data 中包含 { "deleted": true }

删除文件夹会将其中的媒体返回到主库。它不会删除媒体、更改媒体 ID 或 URL,也不会更改媒体使用记录。

文件夹错误

状态代码原因
400INVALID_CURSOR文件夹列表游标无效
400VALIDATION_ERROR文件夹名称、文件夹 ID 或列表参数无效
404NOT_FOUND文件夹或文件夹分配目标不存在
409CONFLICT已有文件夹具有相同的规范化名称

删除媒体

DELETE /_emdash/api/media/:id

启用媒体使用跟踪

关闭了媒体使用跟踪的站点需要开启一次。在 EmDash 准备每个集合时,暂停对数据库的直接写入。EmDash 在此步骤中临时阻止自身的内容和 schema 写入。

两个端点都需要 schema:manage。Bearer 令牌还需要 admin 作用域。

有关管理员操作步骤,请参阅开启媒体使用跟踪。以下端点为 API 操作者提供相同的流程。

检查当前状态

GET /_emdash/api/admin/media-usage/activation

此请求不会更改任何内容。它返回以下状态之一:

  • expanded:媒体使用跟踪已关闭。
  • activating:EmDash 正在准备站点的集合。
  • active:EmDash 跟踪内容中媒体引用的更改。

状态响应不包含内部锁数据或原始数据库错误。

开始激活

POST /_emdash/api/admin/media-usage/activation
Content-Type: application/json
X-EmDash-Request: 1

{
	"writersDrained": true
}

该请求最多准备一个集合。成功后,通过下面的进度端点推进设置和历史索引。

在应用程序和直接数据库写入停止且所有正在进行的写入完成后,将 writersDrained 设置为 true

使用 API 启用跟踪

  1. 停止所有直接数据库写入者。等待正在进行的写入完成。EmDash 在设置期间会隔离自身的写入。
  2. 调用激活 GET 端点检查当前状态。
  3. 使用 writersDrained: true 调用一次激活 POST 端点。
  4. 串行调用进度 POST 端点,遵循 nextRequestInMs,直到激活变为 active
  5. 激活变为 active 后恢复直接数据库写入。
  6. 继续进度请求,直到历史索引报告 readynextRequestInMsnull

如果 POST 超时或返回 409500,在决定操作之前先调用 GET。如果状态仍为 activating 且没有 lastErrorCode,则另一个请求可能拥有当前批次。如果设置了 lastErrorCode,保持写入停止,检查应用程序日志,修复问题,然后发送一个确认的 POST 进行重试。不要编辑 EmDash 的内部数据库表。

当状态为 active 时,EmDash 跟踪内容中媒体引用的更改。现有内容可能仍需要进度请求才能完成历史索引。

检查历史索引进度

GET /_emdash/api/admin/media-usage/progress

激活变为 active 后,此端点返回 indexingreadyneeds_attention,以及就绪和总计的当前内容类型数。该端点不检查内容行或返回工作项详情。需要 schema:manage;bearer 令牌还需要 admin 作用域。

推进设置和历史索引

POST /_emdash/api/admin/media-usage/progress
X-EmDash-Request: 1

该请求没有请求体。它运行一个有界维护步骤,并在该步骤后返回存储的激活和进度状态。

{
	"success": true,
	"data": {
		"activation": {
			"state": "active",
			"collectionCursor": null,
			"attemptCount": 2,
			"drainConfirmedAt": "2026-08-24T12:00:00.000Z",
			"lastAttemptedAt": "2026-08-24T12:00:01.000Z",
			"lastErrorCode": null,
			"leaseExpiresAt": null,
			"activatedAt": "2026-08-24T12:00:01.000Z",
			"updatedAt": "2026-08-24T12:00:02.000Z"
		},
		"progress": {
			"status": "indexing",
			"readyCollections": 1,
			"totalCollections": 2
		},
		"nextRequestInMs": 0
	}
}

progress 在激活变为 active 之前为 nullnextRequestInMs0 表示立即后续请求,30000 表示延迟重试,null 表示服务器不知道有后续。一次只发送一个进度请求,并等待返回的延迟时间。

关闭客户端不会丢弃已完成的工作,但会停止未来的请求。要恢复,先读取激活状态,当激活为 active 时读取进度,然后继续进度请求。在收到模糊响应后,先执行相同的读取再重试。

列出媒体使用工作

GET /_emdash/api/admin/media-usage/work?collection=posts&state=failed&limit=50&cursor=...

返回一个当前集合的持久条目索引工作的有界页面。该端点需要 schema:manage;bearer 令牌还需要 admin 作用域。

collection 是必需的。state 可选地过滤 pendingretryleasedfailed 工作。limit 默认为 50,上限为 100。cursor 是不透明的,来自上一页的 nextCursor。该端点不计算精确的积压计数。

{
	"success": true,
	"data": {
		"items": [
			{
				"collectionId": "01COLLECTION...",
				"collectionSlug": "posts",
				"contentId": "01CONTENT...",
				"state": "failed",
				"attemptCount": 5,
				"nextAttemptAt": "2026-08-07T12:00:00.000Z",
				"leaseExpiresAt": null,
				"lastAttemptedAt": "2026-08-07T11:45:00.000Z",
				"lastErrorCode": "MEDIA_USAGE_PROCESSING_FAILED",
				"updatedAt": "2026-08-07T11:45:00.000Z"
			}
		],
		"nextCursor": "eyJvcmRlclZhbHVlIjoiLi4uIn0"
	}
}

响应省略工作版本、租约令牌、原始数据库错误、索引内容、媒体引用和精确计数。

重试媒体使用工作

POST /_emdash/api/admin/media-usage/work/retry
Content-Type: application/json
X-EmDash-Request: 1

幂等地重新打开或创建一个持久条目任务。它与列表端点具有相同的授权要求。

{
	"collectionId": "01COLLECTION...",
	"contentId": "01CONTENT..."
}

成功响应返回 changed 和当前的待处理项。changed: false 表示任务已经是待处理状态。未过期的工作者租约返回 409 WORK_LEASE_ACTIVEdetails.leaseExpiresAt;并发变更返回 409 WORK_CHANGED。两种冲突都不会替换较新的工作或暴露其租约令牌。

列表仅返回已知的持久工作。重试可以为已提供的身份在活跃集合中创建工作,即使不存在工作行,但它不扫描历史差距。在导入或直接数据库写入后使用集合范围的媒体使用修复。 失败的任务保持可见且可手动重试。needs_attention 进度状态会停止媒体使用跟踪设置页面的自动请求,直到解决底层故障。

恢复集合删除

GET /_emdash/api/admin/media-usage/collection-deletions?state=failed&limit=50&cursor=...

返回持久集合删除工作的有界页面。列表默认为失败的工作;limit 默认为 50,上限为 100。项包括不可变集合 ID、slug、阶段、尝试次数、资格/租约时间戳、稳定错误代码和更新时间。租约令牌、原始数据库错误、内容、媒体引用和精确积压计数从不返回。

POST /_emdash/api/admin/media-usage/collection-deletions/retry
Content-Type: application/json
X-EmDash-Request: 1

{ "collectionId": "01COLLECTION..." }

重试会重新打开失败的、正在重试的或租约已过期的工作,而不更改其阶段。活跃租约返回 409 WORK_LEASE_ACTIVE;并发状态更改返回 409 WORK_CHANGED。两个路由都需要 schema:manage,bearer 令牌还需要 admin 作用域。它们仅恢复内部索引清理,从不删除媒体资源。

修复媒体使用

POST /_emdash/api/admin/media-usage/repair
Content-Type: application/json
X-EmDash-Request: 1

修复一个或所有内容集合的内容媒体使用索引。这是管理员/操作员端点:会话认证的调用者需要 schema:manage,bearer 令牌必须具有 admin 作用域,因为该路由位于 /_emdash/api/admin 下。

全内容修复在当前版本中同步且顺序运行。在大型站点上可能代价较高,因此调用者应有意触发并等待响应。

请求体

修复一个集合:

{
	"scope": "collection",
	"collection": "posts"
}

修复所有内容集合:

{
	"scope": "all"
}

请求体是必需的。无效的 slug、未知的请求键、缺少 scope 和无请求体的请求返回 400,而不是默认为全内容修复。

响应

该端点在修复调用产生结构化结果时返回 200。检查 data.statusfailedstale 是修复领域状态,不是传输错误。

{
	"data": {
		"status": "complete",
		"indexedSourceCount": 12,
		"failedSourceCount": 0,
		"skippedSourceCount": 0,
		"deletedSourceCount": 1,
		"collections": [
			{
				"collection": "posts",
				"status": "complete",
				"indexedSourceCount": 12,
				"failedSourceCount": 0,
				"skippedSourceCount": 0,
				"deletedSourceCount": 1,
				"lastErrorCode": null,
				"startedAt": "2026-07-07T12:00:00.000Z",
				"completedAt": "2026-07-07T12:00:01.000Z"
			}
		]
	}
}

顶级响应字段:

字段类型描述
statuscomplete | partial | failed | stale聚合修复状态
indexedSourceCountnumber修复期间索引的源
failedSourceCountnumber修复期间失败的源
skippedSourceCountnumber跳过的源,包括过时冲突
deletedSourceCountnumber修复期间删除的过时使用行
collectionsarray每个集合的修复摘要

集合摘要字段:

字段类型描述
collectionstring集合 slug
statuscomplete | partial | failed | stale集合修复状态
indexedSourceCountnumber此集合索引的源
failedSourceCountnumber此集合失败的源
skippedSourceCountnumber此集合跳过的源
deletedSourceCountnumber此集合删除的过时使用行
lastErrorCodestring | null最后的集合修复错误(如有)
startedAtstring修复开始时间
completedAtstring | null完成时间,过时结果为 null

未知集合返回 200data.status"failed",每个集合包含 lastErrorCode(如 COLLECTION_NOT_FOUND)。传输错误仍使用标准错误信封,包括 400401403413500

获取媒体文件

GET /_emdash/api/media/file/:key

提供实际文件内容。仅适用于本地存储。

修订版本端点

列出修订版本

GET /_emdash/api/content/:collection/:entryId/revisions

参数

参数类型描述
limitnumber最大返回修订数(默认:50)

响应

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "01HXK5MZSN...",
        "collection": "posts",
        "entryId": "01HXK5MZSN...",
        "data": { ... },
        "createdAt": "2025-01-24T12:00:00Z"
      }
    ],
    "total": 5
  }
}

获取修订版本

GET /_emdash/api/revisions/:revisionId

恢复修订版本

POST /_emdash/api/revisions/:revisionId/restore

将内容恢复到此修订版本的状态并创建新的修订版本。

Schema 端点

列出集合

GET /_emdash/api/schema/collections

响应

{
	"success": true,
	"data": {
		"items": [
			{
				"id": "01HXK5MZSN...",
				"slug": "posts",
				"label": "Posts",
				"labelSingular": "Post",
				"supports": ["drafts", "revisions", "preview"]
			}
		]
	}
}

获取集合

GET /_emdash/api/schema/collections/:slug

参数

参数类型描述
includeFieldsboolean包含字段定义(查询参数)

创建集合

POST /_emdash/api/schema/collections
Content-Type: application/json

请求体

{
	"slug": "products",
	"label": "Products",
	"labelSingular": "Product",
	"description": "Product catalog",
	"supports": ["drafts", "revisions"]
}

更新集合

PUT /_emdash/api/schema/collections/:slug
Content-Type: application/json

删除集合

DELETE /_emdash/api/schema/collections/:slug

参数

参数类型描述
forceboolean即使集合有内容也删除(查询参数)

列出字段

GET /_emdash/api/schema/collections/:slug/fields

创建字段

POST /_emdash/api/schema/collections/:slug/fields
Content-Type: application/json

请求体

{
	"slug": "price",
	"label": "Price",
	"type": "number",
	"required": true,
	"validation": {
		"min": 0
	}
}

更新字段

PUT /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug
Content-Type: application/json

删除字段

DELETE /_emdash/api/schema/collections/:collectionSlug/fields/:fieldSlug

重新排序字段

POST /_emdash/api/schema/collections/:slug/fields/reorder
Content-Type: application/json

请求体

{
	"fieldSlugs": ["title", "content", "author", "publishedAt"]
}

Schema 导出

导出 Schema(JSON)

GET /_emdash/api/schema
Accept: application/json

导出 Schema(TypeScript)

GET /_emdash/api/schema?format=typescript
Accept: text/typescript

返回所有集合的 TypeScript 接口。

插件端点

列出插件

GET /_emdash/api/admin/plugins

获取插件

GET /_emdash/api/admin/plugins/:id

启用插件

POST /_emdash/api/admin/plugins/:id/enable

禁用插件

POST /_emdash/api/admin/plugins/:id/disable

错误代码

代码HTTP 状态描述
NOT_FOUND404资源未找到
VALIDATION_ERROR400输入数据无效
UNAUTHORIZED401缺少或无效的令牌
FORBIDDEN403权限不足
CONTENT_LIST_ERROR500列出内容失败
CONTENT_CREATE_ERROR500创建内容失败
CONTENT_UPDATE_ERROR500更新内容失败
SAVE_REJECTED422保存被插件钩子拒绝
CONTENT_HOOK_ERROR500保存期间插件钩子失败
CONTENT_DELETE_ERROR500删除内容失败
MEDIA_LIST_ERROR500列出媒体失败
MEDIA_CREATE_ERROR500创建媒体失败
SCHEMA_CREATE_ERROR500Schema 操作失败
SLUG_CONFLICT409Slug 已存在
RESERVED_SLUG400Slug 是保留的

搜索端点

全局搜索

GET /_emdash/api/search?q=hello+world

参数

参数类型描述
qstring搜索查询(必需)
collectionsstring逗号分隔的集合 slug
statusstring按状态过滤(默认:published)
limitnumber最大结果数(默认:20)
cursorstring分页游标

响应

{
  "success": true,
  "data": {
    "items": [
      {
        "collection": "posts",
        "id": "01HXK5MZSN...",
        "slug": "hello-world",
        "locale": "en",
        "title": "Hello World",
        "snippet": "...this is a <mark>hello</mark> <mark>world</mark> example...",
        "score": 0.95
      }
    ],
    "nextCursor": "eyJvZmZzZXQiOjIwfQ"
  }
}

搜索建议

GET /_emdash/api/search/suggest?q=hel&limit=5

返回用于自动完成的前缀匹配标题。

配置集合搜索

POST /_emdash/api/search/enable
Content-Type: application/json

{
  "collection": "posts",
  "enabled": true,
  "tokenize": "trigram",
  "weights": {
    "title": 10,
    "content": 1
  }
}

可选的 tokenize 字段控制 SQLite FTS5 如何索引集合。在已启用的集合上更改它会重建并重新填充该集合的搜索索引。

适用场景
porter unicode61默认。受益于 Porter 词干提取的英语内容,例如匹配相关词形。Porter 词干提取是英语特定的。
unicode61使用单词分隔符但不应使用英语词干提取的语言。
trigram文本不以空格分隔的语言,包括日语、中文、泰语、高棉语、老挝语和缅甸语,或需要子串匹配时。短于三个 Unicode 字符的查询不返回匹配项。

在没有存储分词器的集合上省略 tokenize 使用 porter unicode61。禁用搜索会保留配置的分词器,供下次启用操作使用。

重建搜索索引

POST /_emdash/api/search/rebuild
Content-Type: application/json

{
  "collection": "posts"
}

使用其存储的分词器和字段权重为指定集合重建 FTS 索引。

搜索统计

GET /_emdash/api/search/stats

返回每个集合的索引文档计数。

区块端点

列出区块

GET /_emdash/api/sections
GET /_emdash/api/sections?source=theme
GET /_emdash/api/sections?search=newsletter

获取区块

GET /_emdash/api/sections/:slug

创建区块

POST /_emdash/api/sections
Content-Type: application/json

{
  "slug": "my-section",
  "title": "My Section",
  "keywords": ["keyword1"],
  "content": [...]
}

更新区块

PUT /_emdash/api/sections/:slug

删除区块

DELETE /_emdash/api/sections/:slug

设置端点

获取所有设置

GET /_emdash/api/settings

更新设置

POST /_emdash/api/settings
Content-Type: application/json

{
  "siteTitle": "My Site",
  "tagline": "A great site",
  "postsPerPage": 10
}

菜单端点

列出菜单

GET /_emdash/api/menus

获取菜单

GET /_emdash/api/menus/:name

创建菜单

POST /_emdash/api/menus
Content-Type: application/json

{
  "name": "footer",
  "label": "Footer Navigation"
}

更新菜单

PUT /_emdash/api/menus/:name

删除菜单

DELETE /_emdash/api/menus/:name

添加菜单项

POST /_emdash/api/menus/:name/items
Content-Type: application/json

{
  "type": "page",
  "referenceCollection": "pages",
  "referenceId": "page_about",
  "label": "About Us"
}

重新排序菜单项

POST /_emdash/api/menus/:name/reorder
Content-Type: application/json

{
  "items": [
    { "id": "item_1", "parentId": null, "sortOrder": 0 },
    { "id": "item_2", "parentId": null, "sortOrder": 1 },
    { "id": "item_3", "parentId": "item_2", "sortOrder": 0 }
  ]
}

分类法端点

列出分类法定义

GET /_emdash/api/taxonomies

获取分类法

GET /_emdash/api/taxonomies/:name

分类法每个语言环境有一个定义。locale 选择返回哪一个。省略时,EmDash 返回站点默认语言环境的定义,在默认语言环境没有定义时回退到最小语言环境代码。该端点需要 taxonomies:read

参数

参数类型描述
namestring分类法名称(路径参数)
localestring定义的语言环境(查询参数)

响应

{
  "success": true,
  "data": {
    "taxonomy": {
      "id": "01HXK5MZSN...",
      "name": "genre",
      "label": "Genres",
      "labelSingular": "Genre",
      "hierarchical": true,
      "collections": ["books", "movies"],
      "locale": "en",
      "translationGroup": "01HXK5MZSN..."
    }
  }
}

collections 仅列出仍然存在的集合。在添加到分类法后被删除的集合会从响应中过滤掉,但保留在存储中,因此重新创建该集合会恢复链接。

分类法的每个语言环境共享一个 translationGroup。未翻译的分类法的 id 就在那里,如上所示。

创建分类法

POST /_emdash/api/taxonomies
Content-Type: application/json

{
  "name": "genre",
  "label": "Genres",
  "labelSingular": "Genre",
  "hierarchical": true,
  "collections": ["books", "movies"]
}

更新分类法

PUT /_emdash/api/taxonomies/:name
Content-Type: application/json

{
  "label": "Categories",
  "labelSingular": "Category",
  "hierarchical": true,
  "collections": ["books"]
}

每个字段都是可选的,省略的字段保留其存储值。发送 "labelSingular": null 清除它。命名不存在的集合返回 VALIDATION_ERROR 且不写入任何内容。响应是更新后的定义,格式与获取分类法相同。该端点需要 taxonomies:manage

请求写入单个语言环境的定义,由 locale 选择。在翻译的分类法上明确传递它:如果不传,写入会落在语言环境代码最小的定义上。寻址没有定义的语言环境返回 NOT_FOUND — 与获取分类法不同,该端点从不回退到其他语言环境的行。

不要在请求体中发送 namelocale。两者都标识正在写入的定义而不是要更改的值,因此请求体会以 VALIDATION_ERROR 拒绝它们而不是忽略它们。分类法无法重命名,因为其术语以 name 为键。

删除分类法

DELETE /_emdash/api/taxonomies/:name

删除所有语言环境中的分类法:每个语言环境的定义、该名称下的每个术语,以及这些术语到内容的每个分配。内容条目本身不会被删除;它们会失去术语分配。

没有 locale 参数,当分类法仍有术语时 EmDash 不会拒绝请求。该端点需要 taxonomies:manage

响应

{
  "success": true,
  "data": { "deleted": true }
}

列出分类法翻译

GET /_emdash/api/taxonomies/:name/translations

列出分类法定义已翻译到的每个语言环境。分类法的任何语言环境都返回相同的列表,因此 locale 仅选择哪个定义解析组。该端点需要 taxonomies:read

响应

{
  "success": true,
  "data": {
    "translationGroup": "01HXK5MZSN...",
    "translations": [
      { "id": "01HXK5MZSN...", "name": "genre", "label": "Genres", "locale": "en" },
      { "id": "01HXK6P2QT...", "name": "genre", "label": "Géneros", "locale": "es" }
    ]
  }
}

要添加语言环境,使用相同的 name、新的 locale 和设置为此列表中某个定义 idtranslationOf 发布到创建分类法

列出术语

GET /_emdash/api/taxonomies/:name/terms

术语按手动顺序返回(参见重新排序术语)。新术语添加到其兄弟组的末尾。

创建术语

POST /_emdash/api/taxonomies/:name/terms
Content-Type: application/json

{
  "slug": "tutorials",
  "label": "Tutorials",
  "parentId": "term_abc",
  "description": "How-to guides"
}

更新术语

PUT /_emdash/api/taxonomies/:name/terms/:slug

删除术语

DELETE /_emdash/api/taxonomies/:name/terms/:slug

重新排序术语

POST /_emdash/api/taxonomies/:name/reorder
Content-Type: application/json

{
  "parentId": "term_abc",
  "ids": ["term_news", "term_featured"]
}

设置一个兄弟组的顺序。parentId 指定其子项正在排序的父项;省略它(或发送 null)表示顶级,对于扁平分类法即所有术语。重新排序从不更改术语的父项 — 使用更新术语来实现。

ids 可以是组的子集:你列出的术语在它们已占据的位置内进行排列,其他每个成员保持其位置。当一个语言环境不渲染整个组时这很重要,这意味着过时的列表不能埋没它遗漏的术语。组外的 id 会被以 REORDER_MISMATCH 拒绝,一次最多可以发送 100 个 id。

因为你遗漏的术语保持其绝对位置,部分列表中的一步移动可以使术语越过该列表未包含的兄弟。如果 [A, B, C] 是完整组,你发送 ["C", "A"] — 因为 B 没有翻译到你正在使用的语言环境 — 结果是 [C, B, A]AC 按要求交换,显示 B 的列表看到 A 移动了两个位置而不是一个。

没有 locale 参数。一个术语在翻译到的每个语言环境中保持一个位置,因此 id 可以是术语 id 或翻译组,在一个语言环境中排序分类法就是在所有语言环境中排序。需要每个语言环境不同顺序的站点应使用单独的分类法。

设置条目术语

POST /_emdash/api/content/:collection/:id/terms/:taxonomy
Content-Type: application/json

{
  "termIds": ["term_news", "term_featured"]
}

小部件区域端点

列出小部件区域

GET /_emdash/api/widget-areas

获取小部件区域

GET /_emdash/api/widget-areas/:name

创建小部件区域

POST /_emdash/api/widget-areas
Content-Type: application/json

{
  "name": "sidebar",
  "label": "Main Sidebar",
  "description": "Appears on posts"
}

删除小部件区域

DELETE /_emdash/api/widget-areas/:name

添加小部件

POST /_emdash/api/widget-areas/:name/widgets
Content-Type: application/json

{
  "type": "content",
  "title": "About",
  "content": [...]
}

更新小部件

PUT /_emdash/api/widget-areas/:name/widgets/:id

删除小部件

DELETE /_emdash/api/widget-areas/:name/widgets/:id

重新排序小部件

POST /_emdash/api/widget-areas/:name/reorder
Content-Type: application/json

{
  "widgetIds": ["widget_1", "widget_2", "widget_3"]
}

用户管理端点

列出用户

GET /_emdash/api/admin/users
GET /_emdash/api/admin/users?role=40
GET /_emdash/api/admin/users?search=john

获取用户

GET /_emdash/api/admin/users/:id

更新用户

PUT /_emdash/api/admin/users/:id
Content-Type: application/json

{
  "name": "John Doe",
  "role": 40
}

启用用户

POST /_emdash/api/admin/users/:id/enable

禁用用户

POST /_emdash/api/admin/users/:id/disable

身份验证端点

设置状态

GET /_emdash/api/setup/status

返回设置是否完成以及是否存在用户。

Passkey 登录

POST /_emdash/api/auth/passkey/options

获取 WebAuthn 身份验证选项。

POST /_emdash/api/auth/passkey/verify
Content-Type: application/json

{
  "id": "credential-id",
  "rawId": "...",
  "response": {...},
  "type": "public-key"
}

验证 passkey 并创建会话。

魔法链接

POST /_emdash/api/auth/magic-link/send
Content-Type: application/json

{
  "email": "[email protected]"
}
GET /_emdash/api/auth/magic-link/verify?token=xxx

登出

POST /_emdash/api/auth/logout

当前用户

GET /_emdash/api/auth/me

邀请用户

POST /_emdash/api/auth/invite
Content-Type: application/json

{
  "email": "[email protected]",
  "role": 30
}

Passkey 管理

GET /_emdash/api/auth/passkey

列出用户的 passkey。

POST /_emdash/api/auth/passkey/register/options
POST /_emdash/api/auth/passkey/register/verify

注册新 passkey。

PATCH /_emdash/api/auth/passkey/:id
Content-Type: application/json

{
  "name": "MacBook Pro"
}

重命名 passkey。

DELETE /_emdash/api/auth/passkey/:id

删除 passkey。

导入端点

分析 WordPress 导出

POST /_emdash/api/import/wordpress/analyze
Content-Type: multipart/form-data

file: <WXR file>

执行 WordPress 导入

POST /_emdash/api/import/wordpress/execute
Content-Type: application/json

{
  "analysisId": "...",
  "options": {
    "includeMedia": true,
    "includeTaxonomies": true,
    "includeMenus": true
  }
}

速率限制

API 端点可能根据部署配置进行速率限制。当被限制时,响应包含:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

CORS

API 支持浏览器请求的 CORS。在部署中配置允许的来源。