本页介绍 Astro 页面、布局和组件用来读取并展示 EmDash 站点的公共 API。它并不列出 emdash 包根目录导出的全部内容:数据库仓库(repository)、API 处理器、迁移工具、插件编写 API 以及其他服务端内部实现,都有各自的参考文档,或仅供框架集成代码使用。
本参考中的站点模板 API 契约列出了 EmDash 官方维护的站点模板从 emdash 导入的每一个函数。MediaValue 之类仅作类型导入的内容,属于相应的数据模型文档。相关函数如果对站点作者有帮助,会在同一节中一并说明,但本页不涉及无关的根导出。
站点模板 API 契约
官方维护的模板会调用以下运行时辅助函数:
| 函数 | 用途 |
|---|---|
decodeSlug | 在查询前解码动态路由参数 |
getEmDashCollection | 读取并筛选 collection 中的条目 |
getEmDashEntry | 按 ID 或 slug 读取单个条目 |
getMenuWithCacheHint | 渲染导航菜单,并带有缓存失效提示 |
getSeoMeta | 解析条目的 SEO 面板取值和模板回退值 |
getSiteSettings | 读取公开的站点标识及其他全局设置 |
getSiteSettingsWithCacheHint | 读取全局设置,并带有缓存失效提示 |
getTaxonomyTermsWithCacheHint | 渲染分类法筛选器,并带有缓存失效提示 |
getTermsForEntries | 为一组条目批量加载某一种分类法 |
sanitizeHref | 在渲染已存储的链接之前,拒绝不安全的 URL scheme |
search | 跨 collection 搜索已发布的内容 |
内容查询
EmDash 的查询函数遵循 Astro 的实时内容集合(live content collections)模式,返回 { entries, error } 或 { entry, error },以便优雅地处理错误。
getEmDashCollection()
获取某个 collection 中的全部条目。以下示例加载所有文章并检查是否出错:
import { getEmDashCollection } from "emdash";
const { entries: posts, error } = await getEmDashCollection("posts");
if (error) {
console.error("Failed to load posts:", error);
}
参数
| 参数 | 类型 | 说明 |
|---|---|---|
collection | string | Collection slug |
options | CollectionFilter | 可选的筛选选项 |
选项
options 参数接受以下筛选条件:
interface WhereRange {
gt?: string;
gte?: string;
lt?: string;
lte?: string;
}
interface CollectionFilter {
status?: "draft" | "published" | "archived";
limit?: number;
cursor?: string; // Keyset pagination — pass a previous `nextCursor`
offset?: number; // Offset pagination — skip N entries (use with `limit`)
where?: Record<string, string | string[] | WhereRange>;
orderBy?: Record<string, "asc" | "desc">;
locale?: string;
}
cursor 与 offset 互斥。where 的键可以是内容字段、分类法或 byline;范围对象可用于有序比较。
返回值
该函数会解析为一个 CollectionResult:
interface CollectionResult<T> {
entries: ContentEntry<T>[]; // Empty array if error or none found
error?: Error; // Set if query failed
cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
nextCursor?: string; // Cursor for the next keyset page, if any
hasMore?: boolean; // Whether more entries exist beyond this page (when `limit` is set)
}
示例
以下示例按状态和分类法筛选、限制结果数量并处理错误:
// Get all published posts
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Get latest 5 posts
const { entries: latest } = await getEmDashCollection("posts", {
limit: 5,
status: "published",
});
// Filter by taxonomy
const { entries: newsPosts } = await getEmDashCollection("posts", {
status: "published",
where: { category: "news" },
});
// Numbered archive page (e.g. /page/3) with offset pagination
const perPage = 20;
const page = Number(Astro.params.page ?? 1);
const { entries: pagePosts, hasMore } = await getEmDashCollection("posts", {
status: "published",
limit: perPage,
offset: (page - 1) * perPage,
orderBy: { published_at: "desc" },
});
// Handle errors
const { entries, error } = await getEmDashCollection("posts");
if (error) {
return new Response("Server error", { status: 500 });
}
getEmDashEntry()
按 slug 或 ID 获取单个条目。以下示例加载一篇文章,并在文章不存在时渲染站点的 404 页面:
import { getEmDashEntry } from "emdash";
const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");
if (!post) {
return Astro.rewrite("/404");
}
参数
| 参数 | 类型 | 说明 |
|---|---|---|
collection | string | Collection slug |
slugOrId | string | 条目 slug 或 ID |
options | { locale?: string; references?: ReferenceSelection } | 可选。用于解析 slug 的 locale,以及要加载的引用字段 |
预览模式会自动处理:当请求带有有效的 _preview 令牌时,查询会返回草稿内容。预览状态无需任何参数。
references 以字段 slug 为键,指明要加载的引用字段。true 表示按默认上限 50 条请求第一页;对象形式可传入 limit(最大 100)以及上一页返回的 cursor。未列出的字段不会被读取,省略该选项的调用不会执行任何引用查询。
返回值
该函数会解析为一个 EntryResult:
interface EntryResult<T, R> {
entry: ContentEntry<T, R> | null; // null if not found
error?: Error; // Set only for actual errors, not "not found"
isPreview: boolean; // true if draft content is being served
fallbackLocale?: string; // Set when locale fallback returned another locale
cacheHint: CacheHint; // Tags and last-modified time for Astro route caching
}
示例
以下示例按 slug 和 ID 获取条目、读取预览状态,并区分错误与“未找到”:
// Get by slug
const { entry: post } = await getEmDashEntry("posts", "hello-world");
// Get by ID
const { entry: post } = await getEmDashEntry("posts", "01HXK5MZSN0FVXT2Q3KPRT9M7D");
// Preview is automatic — isPreview is true when a valid _preview token is present
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
// Handle errors vs not-found
if (error) {
return new Response("Server error", { status: 500 });
}
if (!entry) {
return Astro.rewrite("/404");
}
getTranslations()
按 collection slug 和数据库 ID 获取某个条目的所有可用翻译:
import { getTranslations } from "emdash";
const { translations, error } = await getTranslations("posts", post.data.id);
结果包含共享的 translationGroup、一个 translations 数组以及可选的 error。每个翻译摘要都包含 id、locale、slug 和 status。
resolveEmDashPath()
根据为可路由 collection 配置的 URL 模式,解析一个公开的路径名:
import { resolveEmDashPath } from "emdash";
const result = await resolveEmDashPath("/blog/hello-world");
if (result) {
console.log(result.collection, result.entry.data.title);
}
结果包含匹配到的 collection、entry 以及路由 params。没有任何已配置的 URL 模式匹配时,该函数返回 null。
getEditMeta()
读取附加在 Portable Text 值上的不可枚举的可视化编辑元数据:
import { getEditMeta } from "emdash";
const meta = getEditMeta(post.data.content);
对于带有标注的值,它返回 { collection, id, field };如果该值没有标注,则返回 undefined。
内容类型
ContentEntry
查询函数返回的条目具有以下形状:
interface ContentEntry<T = Record<string, unknown>, R = ReferencePages> {
id: string;
data: T;
references?: R; // One page per reference field requested; absent otherwise
edit: EditProxy; // Visual editing annotations
}
edit 代理提供可视化编辑标注。将它展开到元素上即可启用行内编辑:{...entry.edit.title}。在非编辑模式下,它不会产生任何输出。
data 对象包含所有内容字段以及系统字段:
id- 唯一标识符slug- URL 友好的标识符status- “draft” | “published” | “archived”createdAt- 创建时间,类型为DateupdatedAt- 最后更新时间,类型为DatepublishedAt- 发布时间,类型为Date或null;内容取消发布后仍会保留- 以及在 collection schema 中定义的所有自定义字段
ReferencePage
entry.references 为 getEmDashEntry 被要求加载的每个引用字段保存一页数据,以字段 slug 为键:
interface ReferencePage<T = Record<string, unknown>> {
entries: ContentEntry<T>[];
nextCursor?: string; // Set when the field holds more entries than the limit
}
每个条目的映射方式与直接加载的条目相同,只有一个例外:署名和分类法术语不会被填充,因此 data.bylines 和 data.terms 不存在。
getEmDashReferences()
获取单个引用字段的一页数据,而无需重新读取其所属的条目。使用上一页返回的 nextCursor 可以继续翻过第一页。
| 参数 | 类型 | 说明 |
|---|---|---|
collection | string | 持有该字段的条目所在的 collection slug |
slugOrId | string | 条目 slug 或 ID |
field | string | 引用字段 slug |
options | { limit?: number; cursor?: string; locale?: string } | 可选。默认 50 条,最多 100 条 |
以下示例加载接下来的 20 个关联条目:
import { getEmDashReferences } from "emdash";
const { entries, nextCursor, error } = await getEmDashReferences(
"posts",
post.id,
"related_posts",
{ cursor, limit: 20 },
);
error 仅在发生真正的错误时设置。未知字段、不存在的条目,或请求无权查看的条目,都会解析为空的 entries 数组。
结果还带有一个 cacheHint,指明该页读取过的行。如果某个路由会缓存其渲染结果,应当把它合并到自己的提示中,做法与 getEmDashEntry({ references }) 合并第一页提示的方式相同,这样写入其中任何一个条目时,该页面都会失效。
URL 辅助函数
decodeSlug() 与 slugify()
在把动态路由参数传给内容查询之前,请先对它使用 decodeSlug()。参数缺失或百分号编码格式错误时它返回 undefined,否则返回解码后的 slug:
function decodeSlug(raw: string | undefined): string | undefined;
import { decodeSlug, getEmDashEntry } from "emdash";
const slug = decodeSlug(Astro.params.slug);
const { entry } = slug ? await getEmDashEntry("posts", slug) : { entry: null };
slugify(value) 会把文本转换为小写、以连字符分隔的 slug。当模板需要根据标签构造 slug 时使用它;已存储的条目 slug 本来就由 EmDash 生成。
sanitizeHref() 与 isSafeHref()
这两个辅助函数接受 http:、https:、mailto:、tel:、站点相对路径和片段标识符。它们会拒绝能够执行脚本的 scheme、协议相对 URL、浏览器会解析到其他站点的反斜杠前缀形式,以及控制字符。isSafeHref(value) 返回布尔值。sanitizeHref(value) 返回原始的安全 URL;当值为空或不安全时返回 "#"。
import { sanitizeHref } from "emdash";
const href = sanitizeHref(menuItem.url);
预览系统
generatePreviewToken()
为草稿内容生成预览令牌。以下示例创建一个一小时后过期的令牌:
import { generatePreviewToken } from "emdash";
const token = await generatePreviewToken({
contentId: "posts:01HXK5MZSN...",
secret: process.env.EMDASH_PREVIEW_SECRET!,
expiresIn: 3600, // 1 hour
});
contentId 必须采用 collection:id 格式。expiresIn 可以是秒数,也可以是以 s、m、h、d 或 w 结尾的时长,默认值为 "1h"。签名密钥只能保存在服务端。
verifyPreviewToken()
验证预览令牌并读取其载荷:
import { verifyPreviewToken } from "emdash";
const result = await verifyPreviewToken({
token,
secret: process.env.EMDASH_PREVIEW_SECRET!,
});
if (result.valid) {
const { cid, exp, iat } = result.payload;
// cid is "collection:id" format, e.g. "posts:my-draft-post"
}
请连同签名用的 secret 传入 token 或 url 之一。无效的令牌会返回 { valid: false, error },其中 error 为 "none"、"malformed"、"invalid" 或 "expired"。
parseContentId()
把预览载荷中的 collection:id 值拆分为两部分:
import { parseContentId } from "emdash";
const parsed = parseContentId(result.payload.cid);
它返回 { collection, id },如果该值不含冒号分隔符,则会抛出异常。
getPreviewUrl() 与 buildPreviewUrl()
getPreviewUrl() 创建并签名一个预览 URL。它接受 collection、id 和 secret,以及可选的 expiresIn、baseUrl、pathPattern 和 locale:
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: post.id,
secret: process.env.EMDASH_PREVIEW_SECRET!,
pathPattern: "/blog/{id}",
});
未提供 baseUrl 时,它返回站点相对 URL。如果已经有令牌,请使用 buildPreviewUrl({ path, token, baseUrl? })。
isPreviewRequest()
检查请求是否带有预览令牌,然后读取它:
import { isPreviewRequest, getPreviewToken } from "emdash";
if (isPreviewRequest(Astro.url)) {
const token = getPreviewToken(Astro.url);
// Verify and show preview content
}
getPreviewToken() 返回 _preview 查询参数;如果不存在,则返回 null。EmDash 中间件会验证常规的预览请求,并自动向 getEmDashEntry() 提供预览状态;这些辅助函数用于自定义预览路由和工具。
内容转换器
在 Portable Text 和 ProseMirror 格式之间转换:
import { prosemirrorToPortableText, portableTextToProsemirror } from "emdash";
// From ProseMirror (editor) to Portable Text (storage)
const portableText = prosemirrorToPortableText(prosemirrorDoc);
// From Portable Text to ProseMirror
const prosemirrorDoc = portableTextToProsemirror(portableText);
ProseMirror 文档对内置块使用 EmDash 自己的节点,例如 image、table、iframeBlock 和 videoBlock。加载它的 TipTap 编辑器需要在其 schema 中包含这些节点,否则无法加载使用了它们的文档。videoBlock 节点具有 src、mediaId、provider、caption、width 和 height 属性。
站点设置
使用 getSiteSettings 和 getSiteSetting 读取站点范围的设置:
function getSiteSettings(): Promise<Partial<SiteSettings>>;
function getSiteSetting<K extends SiteSettingKey>(key: K): Promise<SiteSettings[K] | undefined>;
import { getSiteSettings, getSiteSetting } from "emdash";
// Get all settings
const settings = await getSiteSettings();
// Get single setting
const title = await getSiteSetting("title");
设置在运行时 API 中是只读的。如需更新,请使用管理 API。
getSiteSettings() 返回的是部分对象,因为未设置的键会被省略。站点 logo 和 favicon 等媒体设置,会在函数返回之前被解析为媒体引用对象。
getSiteSettingsWithCacheHint() 返回 { data, cacheHint }。当站点设置变更后需要让 Astro 路由缓存失效时,请把该提示传给 Astro.cache.set()。
SEO
使用 getSeoMeta() 解析页面的 SEO 面板取值和内容回退值:
function getSeoMeta<T>(content: SeoContentInput<T>, options?: SeoMetaOptions): SeoMeta;
interface SeoMetaOptions {
siteTitle?: string;
siteUrl?: string;
titleSeparator?: string; // Default: " | "
path?: string;
defaultOgImage?: string;
defaultTitle?: string;
defaultDescription?: string;
}
interface SeoMeta {
title: string;
description: string | null;
ogTitle: string;
ogDescription: string | null;
ogImage: string | null;
canonical: string | null;
robots: string | null;
}
import { getSeoMeta } from "emdash";
const meta = getSeoMeta(post, {
siteTitle: "Example Blog",
siteUrl: "https://example.com",
path: `/blog/${post.data.slug}`,
});
它返回解析后的标题、描述、Open Graph 值、规范 URL 和 robots 值。getContentSeo(content) 返回原始的 SEO 对象,不应用模板回退值。
对于已翻译的内容,getHreflangAlternates(collection, entryId, { siteUrl? }) 会以 { hreflang, href } 对象的形式返回已发布且可路由的各 locale 版本,并追加一个 x-default 条目。当国际化已禁用、当前条目被标记为 noindex,或没有可用的绝对站点 URL 时,它返回空数组。
评论
获取某个条目的已批准评论及其数量:
import { getCommentCount, getComments } from "emdash";
const { items: comments, total } = await getComments({
collection: "posts",
contentId: post.data.id,
threaded: true,
reactions: true,
sort: "best",
});
const count = await getCommentCount("posts", post.data.id);
threaded 会把回复嵌套在其父评论之下。默认的 sort 为 "oldest";"best" 会按反应数对顶层评论排序,并自动启用反应计数。服务端渲染的评论查询最多返回 500 条已批准评论;如果客户端需要分页,请使用 REST API。
翻译评论组件
emdash/ui/comments 中的 Comments 和 CommentForm 组件默认渲染英文文本。传入 labels 对象即可替换其中任意文本。你省略的键,或设为 undefined 的键,会保留其英文默认值:
<Comments
collection="posts"
contentId={post.data.id}
threaded
labels={{
heading: (count) => (count === 1 ? "1 Kommentar" : `${count} Kommentare`),
member: "Mitglied",
like: "Gefällt mir",
}}
/>
<CommentForm
collection="posts"
contentId={post.data.id}
labels={{
name: "Name",
email: "E-Mail",
comment: "Kommentar",
submit: "Kommentar senden",
submitting: "Wird gesendet …",
published: "Kommentar veröffentlicht",
pending: "Kommentar wird vor der Veröffentlichung geprüft",
error: "Der Kommentar konnte nicht gesendet werden.",
networkError: "Netzwerkfehler. Bitte versuche es erneut.",
}}
/>
Comments 接受 heading、member 和 like。heading 会收到已批准评论的数量,因此可以为零条评论返回单独的文本。
CommentForm 接受 name、email、comment、submit、submitting、published、pending、error 和 networkError。评论被立即批准时,表单显示 published;等待审核时显示 pending。没有 error 时,表单会显示服务器返回的英文错误消息。
Comments 按页面的区域设置(Astro.currentLocale)格式化评论日期,并回退到 en-US。只有启用了 i18n 配置时,Astro 才会设置 Astro.currentLocale。如果未启用,或想使用其他格式,请传入 BCP 47 区域设置标签,例如 locale="de-CH"。
菜单
获取导航菜单并遍历其项目,包括嵌套的子项:
function getMenu(name: string, options?: { locale?: string }): Promise<Menu | null>;
function getMenus(options?: { locale?: string }): Promise<MenuSummary[]>;
import { getMenu, getMenus } from "emdash";
// Get all menus
const menus = await getMenus();
// Get specific menu with items
const primaryMenu = await getMenu("primary");
if (primaryMenu) {
primaryMenu.items.forEach(item => {
console.log(item.label, item.url);
// Nested items for dropdowns
item.children.forEach(child => console.log(" -", child.label));
});
}
getMenu(name, { locale? }) 遵循已配置的 locale 回退链。getMenus({ locale? }) 列出已解析的请求 locale 或已配置 locale 下的菜单摘要;未启用国际化时,它会列出所有 locale 的菜单。getMenuWithCacheHint() 返回 { data, cacheHint },供使用 Astro 缓存的路由使用。
署名
按 ID 或 slug 获取作者资料,或列出署名到某个 byline 的条目:
import { getByline, getBylineBySlug, getEntriesByByline } from "emdash";
const profile = await getBylineBySlug("jane-doe", { locale: "en" });
const posts = profile
? await getEntriesByByline("posts", profile.translationGroup ?? profile.id)
: [];
getByline(id) 返回一份资料或 null。按 slug 查找可接受可选的 locale,并遵循 locale 回退链。内容查询已经会把条目按顺序排列的署名填充到 entry.data.bylines;作者页和署名归档页请使用这些独立的辅助函数。
分类法
获取分类法术语、单个术语、条目的术语,或按术语获取条目:
function getTaxonomyTerms(
taxonomyName: string,
options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm[]>;
function getTerm(
taxonomyName: string,
slug: string,
options?: { locale?: string; includeCounts?: boolean },
): Promise<TaxonomyTerm | null>;
function getTermsForEntries(
collection: string,
entryIds: string[],
taxonomyName: string,
options?: { locale?: string },
): Promise<Map<string, TaxonomyTerm[]>>;
import { getTaxonomyTerms, getTerm, getEntryTerms, getEntriesByTerm } from "emdash";
// Get all terms for a taxonomy (tree structure for hierarchical)
const categories = await getTaxonomyTerms("category");
// Get single term
const news = await getTerm("category", "news");
// Get terms assigned to a content entry
const postCategories = await getEntryTerms("posts", "post-123", "category");
// Get entries with a specific term
const newsPosts = await getEntriesByTerm("posts", "category", "news");
getTaxonomyDefs({ locale? }) 为每种分类法列出一份定义,getTaxonomyDef(name, { locale? }) 返回一份定义;如果没有任何 locale 定义该分类法,则返回 null。两者都会在所有 locale 中解析分类法;它们使用哪个 locale 的标签,请参阅翻译分类法和术语。术语查询遵循已配置的回退链。
getTaxonomyTerms(name, { locale?, includeCounts? }) 对层级分类法返回一棵树,并默认包含可见条目的数量。如果模板不渲染数量,请传入 includeCounts: false。内容查询同样会把已分配的术语填充到每个条目的 data.terms 中;当你只有 collection 名称和条目 ID 时,请使用 getEntryTerms()。
对于在许多条目旁渲染术语的归档页,请批量查询,而不要在循环中调用 getEntryTerms():
import { getAllTermsForEntries, getTermsForEntries } from "emdash";
const termsByPost = await getTermsForEntries(
"posts",
posts.map(post => post.data.id),
"category",
);
const allTermsByPost = await getAllTermsForEntries(
"posts",
posts.map(post => post.data.id),
);
getTermsForEntries() 返回从条目 ID 到所请求分类法术语的映射。getAllTermsForEntries() 返回从条目 ID 到按分类法名称分组的术语的映射。getTaxonomyTermsWithCacheHint() 返回 { data, cacheHint },供启用 Astro 缓存的路由使用。
小工具区域
获取小工具区域及其包含的小工具:
import { getWidgetArea, getWidgetAreas } from "emdash";
// Get all widget areas
const areas = await getWidgetAreas();
// Get specific widget area with widgets
const sidebar = await getWidgetArea("sidebar");
if (sidebar) {
sidebar.widgets.forEach(widget => {
console.log(widget.type, widget.title);
});
}
getWidgetAreaWithCacheHint(name) 返回 { data, cacheHint },供使用 Astro 缓存的路由使用。小工具区域及其中的小工具按配置的排序顺序排列。
Sections
获取 section 并对其进行筛选:
import { getSection, getSections } from "emdash";
// Get all sections (paginated)
const { items, nextCursor } = await getSections();
// Filter sections
const { items: themeSections } = await getSections({ source: "theme" });
const { items: results } = await getSections({ search: "newsletter" });
// Get a single section by slug
const cta = await getSection("newsletter-cta");
getSections(options?) 返回 { items: Section[]; nextCursor?: string }。可用选项有 source("theme" | "user" | "import")、search、limit(默认 50,最大 100)和 cursor。
搜索
跨 collection 运行全局搜索。结果包含高亮片段:
function search(query: string, options?: SearchOptions): Promise<SearchResponse>;
interface SearchOptions {
collections?: string[]; // Default: every searchable collection
status?: string; // Default: "published"
locale?: string; // Default: all locales
limit?: number; // Default: 20
cursor?: string;
scope?: "all" | "title"; // Default: "all"
}
interface SearchResponse {
items: SearchResult[];
nextCursor?: string;
}
import { search } from "emdash";
const results = await search("hello world", {
collections: ["posts", "pages"],
status: "published",
limit: 20,
});
// search() resolves to { items, nextCursor? }
results.items.forEach(result => {
console.log(result.title);
console.log(result.snippet); // Contains <mark> tags
console.log(result.score);
});
// Paginate: pass the previous nextCursor back as `cursor` to get the next page.
// nextCursor is undefined once there are no more results.
if (results.nextCursor) {
const next = await search("hello world", {
collections: ["posts", "pages"],
limit: 20,
cursor: results.nextCursor,
});
}
使用 scope: "title" 时,查询只匹配每个 collection 的标题字段,而不是完整的已索引文本——这对选择器和自动补全很有用,因为正文匹配会让人意外。标题字段未建立搜索索引的 collection,在此范围下不返回任何结果。
错误处理
内容查询会把运行时错误放在结果中返回,而不是抛出。条目不存在并不算运行时错误:此时 entry 为 null,error 保持未定义。
const { entry, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Content could not be loaded", { status: 500 });
}
if (!entry) {
return Astro.rewrite("/404");
}
没有结果包装的辅助函数,在输入无效或数据库操作失败时可能会抛出异常。如果页面能提供有意义的回退,请在路由边界处理这些错误。较底层的服务端集成所使用的仓库和处理器错误类,不在这套站点模板 API 的范围内。