查詢內容

本頁內容

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 參考。若要圍繞這些 查詢建置一個可運作的範例,請繼續閱讀建立部落格。