JavaScript API 參考

本頁內容

本頁說明 Astro 頁面、版面配置與元件用來讀取並呈現 EmDash 網站的公開 API。它並未列出 emdash 套件根目錄匯出的全部內容:資料庫儲存庫(repository)、API 處理常式、遷移工具、外掛程式撰寫 API 以及其他伺服器端內部實作,都有各自的參考文件,或僅供框架整合程式碼使用。

本參考中的網站範本 API 契約列出了 EmDash 官方維護的網站範本從 emdash 匯入的每一個函式。MediaValue 之類僅作型別匯入的項目,屬於相應的資料模型文件。相關函式若對網站作者有幫助,會在同一節中一併說明,但本頁不涵蓋無關的根匯出。

網站範本 API 契約

官方維護的範本會呼叫以下執行階段輔助函式:

函式用途
decodeSlug在查詢前解碼動態路由參數
getEmDashCollection讀取並篩選集合中的條目
getEmDashEntry依 ID 或 slug 讀取單一條目
getMenuWithCacheHint轉譯導覽選單,並附帶快取失效提示
getSeoMeta解析條目的 SEO 面板值與範本備援值
getSiteSettings讀取公開的網站識別資訊及其他全域設定
getSiteSettingsWithCacheHint讀取全域設定,並附帶快取失效提示
getTaxonomyTermsWithCacheHint轉譯分類法篩選器,並附帶快取失效提示
getTermsForEntries為一組條目批次載入某一種分類法
sanitizeHref在轉譯已儲存的連結之前,拒絕不安全的 URL scheme
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 取得單一條目。以下範例載入一篇文章,並在文章不存在時轉譯網站的 404 頁面:

import { getEmDashEntry } from "emdash";

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

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

參數

參數型別說明
collectionstring集合 slug
slugOrIdstring條目 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()

依集合 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 為 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,即可繼續翻過第一頁。

參數型別說明
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,指明該頁讀取過的資料列。如果某個路由會快取其轉譯結果,應當把它合併到自己的提示中,做法與 getEmDashEntry({ references }) 合併第一頁提示的方式相同,這樣寫入其中任何一個條目時,該頁面都會失效。

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

這兩個輔助函式接受 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);

站台設定

使用 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。

選單

取得導覽選單並走訪其項目,包括巢狀的子項目:

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;當你只有集合名稱與條目 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。

搜尋

跨集合執行全域搜尋。結果包含醒目提示的片段:

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" 時,查詢只比對每個集合的標題欄位,而不是完整的已索引文字——這對選擇器與自動完成很有用,因為本文比對會讓人意外。標題欄位未建立搜尋索引的集合,在此範圍下不傳回任何結果。

錯誤處理

內容查詢會把執行階段錯誤放在結果中傳回,而不是擲回。條目不存在並不算執行階段錯誤:此時 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 的範圍內。