EmDash 頁面在請求時使用 getEmDashCollection() 和 getEmDashEntry() 讀取內容。前者傳回清單,後者按 slug 或內容 ID 傳回一筆項目。兩者都將錯誤作為資料傳回,以便頁面決定如何回應。
綁定範本使用 Astro 的伺服器輸出。因此,編輯者發佈後,造訪者會在下一次轉譯時收到最新已發佈修訂。已儲存的草稿在發佈或透過有效預覽 URL 請求之前保持私密。
查詢集合
以下頁面載入最近發佈的七篇文章。公開集合查詢預設 status: "published",因此篩選器無需重複。
---
import { getEmDashCollection } from "emdash";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
limit: 7,
});
if (error) {
console.error("Failed to load posts:", error);
return new Response("Unable to load posts", { status: 500 });
}
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/posts/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
排序與限制在 EmDash 水合項目之前於資料庫中完成。這避免了載入整個集合並在頁面中排序或切片。
結果包含:
entries,在無符合或查詢失敗時為空陣列;error,對失敗的查詢設定,但不對空結果設定;cacheHint,攜帶 Astro 快取的標籤與最後修改時間;nextCursor,當有限的游標頁還有更多項目時設定;以及hasMore,回報有限的游標或偏移頁是否還有下一頁。
項目識別碼
每個結果有兩個職責不同的識別碼:
entry.id是內容載入器產生的面向 URL 的識別碼。通常是 slug。當國際化為地區設定加前綴時,會包含前綴。從集合結果建立連結時使用此值。entry.data.id是資料庫中儲存的穩定內容 ID。編輯者變更 slug 時它不會改變。當 API、分類法助手、頁面情境或關係期望內容 ID 時使用它。
getEmDashEntry() 接受 slug 或穩定內容 ID。啟用國際化時,slug 查詢限定在某個地區設定;內容 ID 直接識別該列。
篩選集合
在第二個參數中傳遞篩選器。以下查詢傳回 news 分類中 series 欄位為 engineering 的已發佈文章:
const { entries } = await getEmDashCollection("posts", {
where: {
category: "news",
series: "engineering",
},
});
命名分類法的鍵符合已指派的術語 slug。其他鍵符合集合欄位。多個鍵以 AND 邏輯組合。一個鍵上的陣列符合任一列出的值,因此 { category: ["news", "updates"] } 符合任一分類。
使用 locale 明確請求一種語言:
const { entries: frenchPosts } = await getEmDashCollection("posts", {
locale: "fr",
orderBy: { published_at: "desc" },
});
若省略 locale,EmDash 使用請求地區設定,然後使用設定的預設地區設定。回退規則與翻譯路由見國際化。
status 接受 "published"、"draft" 或 "archived"。不要從公開路由請求草稿。當造訪者需要暫時存取未發佈項目時,使用預覽流程。
在資料庫中排序結果
orderBy 將欄位名稱對應到 "asc" 或 "desc"。使用儲存的欄位名稱,而非 entry.data 中傳回的 camelCase 名稱:
const { entries } = await getEmDashCollection("posts", {
orderBy: {
published_at: "desc",
title: "asc",
},
});
系統欄使用其資料庫名稱,如 created_at、updated_at 與 published_at。自訂欄位使用其集合 slug,如 title 或 priority。傳回的資料將系統日期對應到 createdAt、updatedAt 與 publishedAt,但這些 camelCase 屬性名稱不是有效的 orderBy 欄位。
EmDash 使用第一個有效的 orderBy 欄位作為分頁鍵,並以內容 ID 作為穩定的決勝局。沒有 orderBy 時,集合預設按 created_at 降序。當網站經常按自訂純量欄位排序或篩選時,請將其標記為已索引;索引可避免集合成長時的全表掃描。
對集合分頁
對連續 feed 使用游標,對編號頁使用偏移。它們是獨立的分頁模型,不能在一個型別化查詢中組合。
游標分頁
游標分頁從上一次查詢傳回的最後一筆項目之後繼續。請求之間保持排序不變,並原樣傳回 nextCursor,不要檢查或變更它。
當存在另一頁時,以下路由轉譯 Older posts 連結:
---
import { getEmDashCollection } from "emdash";
const cursor = Astro.url.searchParams.get("cursor") ?? undefined;
const { entries: posts, nextCursor, error } = await getEmDashCollection("posts", {
limit: 10,
cursor,
orderBy: { published_at: "desc" },
});
if (error) return new Response("Unable to load posts", { status: 500 });
---
<ul>
{posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>
{nextCursor && (
<a href={`/posts?cursor=${encodeURIComponent(nextCursor)}`}>Older posts</a>
)}
最後一頁沒有 nextCursor。游標分頁不計算總頁數,也不提供上一頁游標;若介面需要返回導覽,請在瀏覽器歷程中保留較早的 URL。
偏移分頁
偏移分頁適合 /posts/page/3 這類路由。將頁碼轉換為偏移,並用 hasMore 做下一頁連結:
---
import { getEmDashCollection } from "emdash";
const parsedPage = Number(Astro.params.page ?? "1");
if (!Number.isInteger(parsedPage) || parsedPage < 1) {
return Astro.redirect("/404");
}
const perPage = 10;
const { entries: posts, hasMore, error } = await getEmDashCollection("posts", {
limit: perPage,
offset: (parsedPage - 1) * perPage,
orderBy: { published_at: "desc" },
});
if (error) return new Response("Unable to load posts", { status: 500 });
---
<ul>
{posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>
<nav aria-label="Post pages">
{parsedPage > 1 && <a href={`/posts/page/${parsedPage - 1}`}>Newer posts</a>}
{hasMore && <a href={`/posts/page/${parsedPage + 1}`}>Older posts</a>}
</nav>
偏移必須是非負整數。第 1 頁使用偏移 0,表示「從第一筆項目開始」。偏移分頁易於按頁碼定址,但請求之間新增的項目可能使後面的頁發生移動。當這種移動會造成困惑時,請使用游標。
查詢並轉譯單筆項目
以下執行階段路由按 slug 讀取文章,轉譯其特色圖片與 Portable Text 正文,並區分查詢失敗與缺失項目:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error, isPreview, cacheHint } = await getEmDashEntry("posts", slug);
if (error) {
console.error("Failed to load post:", error);
return new Response("Unable to load post", { status: 500 });
}
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
{isPreview && <p>This is an unpublished preview.</p>}
<article>
{post.data.featured_image && <Image image={post.data.featured_image} priority />}
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
PortableText 為 EmDash 的標準區塊提供轉譯器,包括圖片、圖庫、程式碼、表格與已淨化的 HTML 區塊。當網站新增自己的 Portable Text 區塊時,傳入自訂元件。若取代 htmlBlock 轉譯器,請淨化 HTML,並僅允許網站打算信任的 iframe 主機。
PortableText 在編輯模式下將表格顯示為唯讀預留位置。要在地化其初始標籤,請傳入 tablePlaceholder={translatedLabel}。預設值為 "Table (edit in admin)",不影響已發佈的表格內容。
讀取引用欄位
reference 欄位 將項目連結到另一集合中的項目,關係 介紹如何定義。其值不是 data 的一部分。向 getEmDashEntry 傳入按欄位 slug 鍵控的 references 選項,命名頁面要轉譯的欄位,每個欄位會作為一頁項目傳回:
---
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug, {
references: { author: true, related_posts: { limit: 6 } },
});
if (!post) return Astro.redirect("/404");
const author = post.references?.author.entries[0];
---
<article>
<h1>{post.data.title}</h1>
{author && <p>By {author.data.name}</p>}
<ul>
{post.references?.related_posts.entries.map((related) => (
<li><a href={`/posts/${related.data.slug}`}>{related.data.title}</a></li>
))}
</ul>
</article>
true 請求預設上限 50 筆的第一頁。對包含更多內容的欄位使用 { limit, cursor },每頁最多 100 筆。
每個被引用的項目都是與直接載入相同形狀的 ContentEntry:一個 id、一個帶 Date 物件日期與已解析媒體值的 data 物件,以及作用範圍限定在被引用項目上的 edit 代理,因此在視覺化編輯中點擊卡片會開啟該卡片所關於的項目。署名與分類術語是例外——EmDash 不會將它們水合到被引用項目上,因此請從項目本身讀取 data.bylines 與 data.terms。
當欄位位於關係的父端時,項目按編輯者排列的順序到達。子端的欄位列出指向它的項目,這些項目沒有自己的順序。
該選項在兩個方向都是選擇性啟用。不傳 references 的呼叫不會執行額外查詢,未納入選取的欄位不會被讀取。選取欄位的呼叫每個欄位一次連結查詢,加上每個不同目標集合一次項目查詢,無論每個欄位持有多少項目。
對引用欄位分頁
getEmDashReferences 使用上一頁傳回的游標取得單一欄位的下一頁:
import { getEmDashReferences } from "emdash";
const { entries, nextCursor } = await getEmDashReferences("posts", post.id, "related_posts", {
cursor,
limit: 20,
});
它從與 getEmDashEntry 相同的請求情境讀取草稿可見性,因此在預覽中開始的遍歷會繼續看到待定選取。頁面 id 在 i18n 加前綴時攜帶其地區設定(fr/about),傳回的就是該 id。
在快取輸出的路由上,將頁面傳回的 cacheHint 與路由自身的合併,以便對所命名項目的寫入會使頁面過期。
預覽中的引用欄位
公開轉譯看到已發佈項目與已發佈選取。項目的預覽,或處於視覺化編輯中的編輯者,會看到未發佈項目與項目草稿中暫存的選取,因此預覽連結會顯示頁面發佈後將擁有的引用。
套用 SEO 與編輯功能
對於支援 SEO 的集合,getSeoMeta() 會解析編輯者的 SEO 標題、描述、圖片、規範 URL 與無索引選擇,並帶來自項目的回退。目前部落格範本將該結果傳給其基礎版面配置:
---
import { getSeoMeta } from "emdash";
const seo = getSeoMeta(post, {
siteTitle: "My Blog",
siteUrl: Astro.url.origin,
path: Astro.url.pathname,
});
---
<Base
title={seo.title}
pageTitle={seo.ogTitle}
description={seo.description}
image={seo.ogImage}
canonical={seo.canonical}
robots={seo.robots}
>
<!-- Post content -->
</Base>
包含 <EmDashHead> 的版面配置可將相同的 SEO 欄位與外掛貢獻套用到伺服器轉譯的內容頁。僅讀取 data.title 或 data.excerpt 的手寫中繼標籤不會套用編輯者的規範 URL 或無索引設定。
預覽 URL 無需單獨查詢。中介軟體驗證 _preview 權杖,且 getEmDashEntry() 傳回符合的草稿並帶有 isPreview: true。預覽與視覺化編輯指南 說明了 URL 產生以及用於內嵌編輯的 entry.edit 註解。
產生 TypeScript 型別
開發伺服器根據作用中架構產生 emdash-env.d.ts。在專案的 TypeScript 設定中包含該檔案,以便像 "posts" 這樣的集合名稱自動選取產生的 Post 資料型別。
對於遠端 EmDash 執行個體,CLI 可以取得架構並將型別寫入 .emdash/types.ts:
npx emdash types --url https://cms.example.com
該命令接受 API 權杖或自訂驗證標頭。這些選項見產生型別。
每個繫結到關係的引用欄位的集合都會獲得第二個介面 {Collection}References,註冊在同一 slug 下。getEmDashEntry 將其結果收窄到 references 選項命名的欄位,每頁的項目攜帶目標集合的介面:
const { entry: post } = await getEmDashEntry("posts", "my-post", {
references: { author: true },
});
// post?.references?.author.entries[0] carries the Author interface
// post?.references?.related_posts is a type error: it was not selected
執行階段轉譯與快取
Node.js 與 Cloudflare 部落格範本在 astro.config.mjs 中設定 output: "server"。它們的內容查詢在每次伺服器轉譯時執行,因此新發佈的修訂有資格進入下一次請求。若您有意預先轉譯某條路由,其 HTML 包含建置時可用的內容,只有再次建置後才會改變。
當 Astro 的快取啟用時,將查詢的 cacheHint 傳給 Astro.cache.set()。EmDash 將回應對應到集合與項目標籤,以便發佈可以使受影響的快取頁面失效。除非延遲更新是明確的產品決策,否則不要用很長的固定 Cache-Control 壽命取代該整合。
有關精確簽章與較少見的篩選器,請參見 JavaScript API 參考。要圍繞這些查詢建置可執行的範例,請繼續參閱建立部落格。