JavaScript API 参考

本页内容

本页介绍 Astro 页面、布局与组件用于读取和展示 EmDash 站点的公开 API。它并未罗列 emdash 包根导出的每一项:数据库仓库、API 处理器、迁移工具、插件作者 API 以及其他服务端内部实现另有参考,或面向框架集成代码。

本参考中的站点模板 API 契约列出了 EmDash 维护的站点模板从 emdash 导入的每一个运行时函数。仅作类型导入的符号(如 MediaValue)属于相应数据模型文档。相关函数若在编写站点时有帮助,会在同一章节说明,但本页不涵盖无关的根导出。

站点模板 API 契约

维护中的模板会调用以下运行时辅助函数:

函数用途
decodeSlug在查询前解码动态路由参数
getEmDashCollection读取并筛选集合中的条目
getEmDashEntry按 ID 或 slug 读取单条条目
getMenuWithCacheHint渲染导航菜单并附带缓存失效提示
getSeoMeta解析条目的 SEO 面板值与模板回退
getSiteSettings读取站点公开标识与其他全局设置
getSiteSettingsWithCacheHint读取全局设置并附带缓存失效提示
getTaxonomyTermsWithCacheHint渲染分类法筛选并附带缓存失效提示
getTermsForEntries为条目列表批量加载某一分类法
sanitizeHref在渲染存储的链接前拒绝不安全的 URL 方案
search跨集合搜索已发布内容

内容查询

EmDash 的查询函数遵循 Astro 的 live content collections 模式,返回 { entries, error } 或 { entry, error },便于优雅地处理错误。

getEmDashCollection()

获取集合中的全部条目。以下示例加载所有文章并检查是否有错误:

import { getEmDashCollection } from "emdash";

const { entries: posts, error } = await getEmDashCollection("posts");

if (error) {
	console.error("Failed to load posts:", error);
}

参数

参数类型说明
collectionstring集合 slug
optionsCollectionFilter可选筛选选项

选项

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 获取单条条目。以下示例加载一篇文章,并在缺失时重定向:

import { getEmDashEntry } from "emdash";

const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");

if (!post) {
	return Astro.redirect("/404");
}

参数

参数类型说明
collectionstring集合 slug
slugOrIdstring条目 slug 或 ID
options{ locale?: string; references?: ReferenceSelection }可选。用于 slug 解析的语言区域,以及要加载的引用字段

预览模式会自动处理:当请求带有有效的 _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.redirect("/404");
}

getTranslations()

按集合 slug 与数据库 ID 获取某一条目的可用翻译:

import { getTranslations } from "emdash";

const { translations, error } = await getTranslations("posts", post.data.id);

结果包含共享的 translationGroup、translations 数组以及可选的 error。每条翻译摘要包括 id、locale、slug 与 status。

resolveEmDashPath()

根据为可路由集合配置的 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 - 创建时间(Date)
  • updatedAt - 最后更新时间(Date)
  • publishedAt - 发布时间(Date),或 null;取消发布时仍保留
  • 以及集合 schema 中定义的所有自定义字段

ReferencePage

entry.references 按字段 slug 存放 getEmDashEntry 请求的每个引用字段的一页数据:

interface ReferencePage<T = Record<string, unknown>> {
	entries: ContentEntry<T>[];
	nextCursor?: string; // Set when the field holds more entries than the limit
}

每条 entry 的映射方式与直接加载的 entry 相同,但有一处例外:署名与分类法术语不会水合,因此 data.bylines 与 data.terms 不存在。

getEmDashReferences()

获取单个引用字段的一页数据,而无需重新读取所属条目。可用该页返回的 nextCursor 继续翻页。

参数类型说明
collectionstring持有该字段的条目所在集合 slug
slugOrIdstring条目 slug 或 ID
fieldstring引用字段 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,标明本页读取的行。若路由缓存所渲染内容,应将其合并进自身的 hint,方式与 getEmDashEntry({ references }) 合并第一页 hint 相同,以便在关联条目更新时使页面缓存失效。

URL 辅助函数

decodeSlug() 与 slugify()

在将动态路由参数传给内容查询前,对其使用 decodeSlug()。参数缺失时返回 undefined,否则应用 decodeURIComponent();格式错误的百分号编码会抛出异常:

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()

这些辅助函数会拒绝 javascript: 等不安全的链接方案。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"
}

传入 token 或 url,以及用于签名的 secret。无效令牌返回 { 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);

站点设置

使用 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 路由缓存需要在站点设置变更后失效时,将 hint 传给 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}`,
});

返回解析后的 title、description、Open Graph 值、canonical URL 与 robots。getContentSeo(content) 返回原始 SEO 对象,不应用模板回退。

对于翻译内容,getHreflangAlternates(collection, entryId, { siteUrl? }) 将已发布、可路由的语言变体返回为 { hreflang, href } 对象,并添加 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。

菜单

获取导航菜单并遍历其项(含嵌套子项):

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? }) 遵循配置的语言回退链。getMenus({ locale? }) 列出已解析请求或配置语言下的菜单摘要;未启用国际化时列出所有语言。getMenuWithCacheHint() 为使用 Astro 缓存的路由返回 { data, cacheHint }。

署名

按 ID 或 slug 获取作者资料,或列出某署名下的条目:

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,并遵循语言回退链。内容查询已将条目的有序署名水合到 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? }) 返回单条定义,若没有任何语言定义该分类法则为 null。二者都会在各语言中解析分类法;翻译分类法与术语 说明它们使用哪种语言的标签。术语查询遵循配置的回退链。

getTaxonomyTerms(name, { locale?, includeCounts? }) 对层级分类法返回树结构,默认包含可见条目计数。模板不展示计数时可传 includeCounts: false。内容查询也会将分配的术语水合到各 entry 的 data.terms;若只有集合名与 entry ID,使用 getEntryTerms()。

对于在多条 entry 旁渲染术语的归档页,应批量查询,而不是在循环中反复调用 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() 返回从 entry ID 到所请求分类法术语的映射。getAllTermsForEntries() 返回从 entry ID 到按分类法名称分组的术语的映射。getTaxonomyTermsWithCacheHint() 为 Astro 缓存路由返回 { data, cacheHint }。

小工具区域

获取小工具区域及其包含的小工具:

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) 为使用 Astro 缓存的路由返回 { data, cacheHint }。小工具区域及其小工具按配置的排序顺序排列。

Sections

获取 Sections 并筛选:

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。

搜索

跨集合执行全局搜索。结果包含高亮片段:

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" 时,查询仅匹配各集合的 title 字段,而非完整索引文本——适用于选择器与自动完成,避免正文匹配造成意外。title 字段未纳入搜索索引的集合在此 scope 下不会返回结果。

错误处理

内容查询将操作性错误放在结果中返回,而不是抛出。条目不存在不算操作性错误:entry 为 null,error 仍为 undefined。

const { entry, error } = await getEmDashEntry("posts", slug);

if (error) {
	return new Response("Content could not be loaded", { status: 500 });
}

if (!entry) {
	return Astro.redirect("/404");
}

没有结果封装的辅助函数在输入无效或数据库操作失败时可能抛出异常。若页面能提供有意义的回退,请在路由边界处理这些错误。更低层服务端集成使用的仓库与处理器错误类不在本站点模板 API 范围内。