本頁說明 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 配置 |
search | 跨集合搜尋已發布內容 |
內容查詢
EmDash 的查詢函式遵循 Astro 的即時內容集合模式,回傳 { 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);
}
參數
| 參數 | 型別 | 說明 |
|---|---|---|
collection | string | 集合 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 取得單一項目。下列範例載入一篇文章,若不存在則重新導向:
import { getEmDashEntry } from "emdash";
const { entry: post, error } = await getEmDashEntry("posts", "my-post-slug");
if (!post) {
return Astro.redirect("/404");
}
參數
| 參數 | 型別 | 說明 |
|---|---|---|
collection | string | 集合 slug |
slugOrId | string | 項目 slug 或 ID |
options | { locale?: string; references?: ReferenceSelection } | 選用。用於 slug 解析的語系,以及要載入的參考欄位 |
預覽模式會自動處理:當請求帶有有效的 _preview token 時,查詢會提供草稿內容。無需額外參數即可進入預覽狀態。
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;取消發布後仍保留- 以及集合結構中定義的所有自訂欄位
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
}
每個項目會像直接載入的項目一樣對應,但有一項例外:署名與分類法詞彙不會 hydrate,因此沒有 data.bylines 與 data.terms。
getEmDashReferences()
取得單一參考欄位的一頁資料,無需重新讀取所屬項目。搭配該頁回傳的 nextCursor 可繼續瀏覽後續頁面。
| 參數 | 型別 | 說明 |
|---|---|---|
collection | string | 持有該欄位之項目的集合 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,標示此頁讀取的列。若路由會快取渲染結果,應將其合併到路由自己的 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()
為草稿內容產生預覽 token。下列範例建立一小時後過期的 token:
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()
驗證預覽 token 並讀取其承載內容:
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 或帶簽署 secret 的 url。無效 token 回傳 { 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。若已有 token,請使用 buildPreviewUrl({ path, token, baseUrl? })。
isPreviewRequest()
檢查請求是否包含預覽 token,並讀取該 token:
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" 依 reactions 排序頂層留言,並自動啟用 reaction 計數。伺服器端渲染的留言查詢最多回傳 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 查詢可選語系,並遵循語系後備鏈。內容查詢已將項目的有序署名 hydrate 到 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。內容查詢也會將指派詞彙 hydrate 到各項目的 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() 為使用 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 }。小工具區域與其小工具依設定的排序順序排列。
區段
取得區段並篩選:
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 欄位未納入搜尋索引的集合在此範圍下不會回傳結果。
錯誤處理
內容查詢在結果中回傳操作錯誤,而非拋錯。找不到項目不屬於操作錯誤: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");
}
沒有結果包裝的輔助函式在輸入無效或資料庫操作失敗時可能拋錯。若頁面能提供有意義的後備,請在路由邊界處理這些錯誤。較底層伺服器整合使用的 repository 與處理常式錯誤類別不在本站點模板 API 範圍內。