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 會使用請求的語言環境,然後是設定的預設語言環境。有關後備規則
和翻譯路由,請參閱國際化 (i18n)。
status 接受 "published"、"draft" 或 "archived"。不要在公開
路由中請求草稿。當訪客需要暫時存取某一筆未發佈條目時,請使用預覽流程。
在資料庫中排序結果
orderBy 將欄位名稱對應到 "asc" 或 "desc"。請使用儲存的欄位名稱,而不是 entry.data 中
傳回的駝峰命名:
const { entries } = await getEmDashCollection("posts", {
orderBy: {
published_at: "desc",
title: "asc",
},
});
系統欄使用其資料庫名稱,如 created_at、updated_at 和 published_at。
自訂欄位使用其集合 slug,如 title 或 priority。傳回的資料將系統
日期對應為 createdAt、updatedAt 和 publishedAt,但這些駝峰命名的屬性名稱不是
有效的 orderBy 欄位。
EmDash 使用第一個有效的 orderBy 欄位作為分頁鍵,並以內容 ID 作為穩定的
決勝項。沒有 orderBy 時,集合預設依 created_at 降冪排列。當站台經常依自訂純量
欄位排序或篩選時,請將這些欄位標記為已建立索引;隨著集合成長,索引可避免全表
掃描。
對集合分頁
連續捲動的資訊流用游標,帶頁碼的分頁用位移。它們是相互獨立的分頁 模型,不能在同一個具型別的查詢中組合使用。
游標分頁
游標分頁從上一次查詢傳回的最後一個條目之後繼續。請在多次請求之間保持排序
不變,並原樣傳回 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.rewrite("/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 頁使用位移量零,表示 「從第一個條目開始」。位移分頁便於依頁碼定位,但在兩次請求之間新增的條目 可能使後面的頁發生位移。當這種位移會造成困惑時,請使用游標。
查詢並轉譯單筆條目
下面的執行階段路由依 slug 讀取一篇文章,轉譯其特色圖片和 Portable Text 內文, 並將查詢失敗與條目不存在區分開:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/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.rewrite("/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 區塊和 iframe 區塊。當站台新增自己的 Portable Text 區塊時,請傳入自訂元件。
帶有 isolated: true 的 HTML 區塊會在沙箱 iframe 中轉譯其 html、css 和 js,該 iframe 會依內容自動調整大小。
其他 HTML 區塊會將經過清理的 html 直接轉譯在頁面中。如果取代 htmlBlock 轉譯器,請將隔離區塊傳給
emdash/ui 中的 HtmlBlock,或在 sandbox 不含 allow-same-origin 的 iframe 中轉譯它們。請自行清理內嵌 HTML,
並且只允許站台打算信任的 iframe 主機。
iframe 區塊會透過 emdash/ui 中的 Iframe 轉譯為響應式、延遲載入的 iframe,並帶有 sandbox 和
strict-origin-when-cross-origin 參照來源政策 (referrer policy)。位於你站台自身主機上的頁面會失去同源存取權,
並且只會轉譯 https 來源。
啟用 Astro 的內容安全政策後,
瀏覽器會封鎖隔離 HTML 區塊中的樣式和指令碼,因此這些區塊轉譯時不帶 CSS、JavaScript,也沒有自動高度。iframe 區塊同樣會失去
自訂尺寸,並且政策中的 frame-src 必須允許它們所嵌入的主機。
PortableText 在編輯模式下將表格顯示為唯讀預留位置。若要在地化其初始標籤,請傳入 tablePlaceholder={translatedLabel}。預設值為 "Table (edit in admin)",不影響已發佈的表格內容。
讀取參照欄位
reference 欄位將條目連結到另一個集合中的條目,關聯介紹如何定義。其值不屬於 data。向 getEmDashEntry 傳入 references 選項,以欄位 slug 為鍵,指明頁面要轉譯的欄位,每個欄位都會傳回為一頁條目:
---
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug, {
references: { author: true, related_posts: { limit: 6 } },
});
if (!post) return Astro.rewrite("/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,一個 data 物件(日期為 Date 物件,媒體值已解析),以及一個限定在該被參照條目上的 edit 代理,因此在視覺化編輯中點擊某張卡片時,會開啟該卡片所指向的條目。Byline 和分類法術語是例外——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 和 no-index 選項,並以條目中的值作為後備。目前的部落格範本
把這個結果傳給其基礎版面配置:
---
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 的手寫 meta 標籤
不會套用編輯者設定的標準 URL 或 no-index 設定。
預覽 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 參考。若要圍繞這些 查詢建置一個可運作的範例,請繼續閱讀建立部落格。