預覽模式

本頁內容

EmDash 的預覽系統允許編輯者透過安全的、有時間限制的 URL 檢視未發布的內容。預覽連結使用 HMAC-SHA256 簽署的權杖,您可以與審閱者共用這些連結,而無需公開整個草稿內容。

運作原理

  1. 管理員為草稿文章產生預覽 URL
  2. URL 包含一個帶有過期時間的已簽署 _preview 查詢參數
  3. EmDash 的中介軟體自動驗證權杖並設定請求上下文
  4. 您的範本程式碼正常呼叫 getEmDashEntry() — 草稿內容會自動提供

預覽是隱式的。中介軟體驗證權杖,查詢函式透過 AsyncLocalStorage 讀取它,因此相同的範本程式碼在預覽期間提供草稿內容,在其他情況下提供已發布的內容。

設定預覽

預覽在安裝 EmDash 後即可運作。首次使用時,EmDash 會產生每個網站的預覽密鑰並將其儲存在資料庫中,因此常見情況不需要組態。

僅在需要時才在環境中設定 EMDASH_PREVIEW_SECRET

  • 在多個程序之間共用密鑰(例如,一個單獨的預覽 Worker,它對 URL 進行簽署並將其傳送到主網站進行驗證)
  • 出於合規性/稽核原因,將密鑰固定為您控制的值
  • 從備份還原時遷移到已知值
# 可選:覆寫自動產生的密鑰
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

如果設定,環境值優先於儲存在資料庫中的值。

現有範本會自動與預覽配合使用,如下面的頁面所示:

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;

// 無需特殊的預覽處理 — 中介軟體
// 檢測 _preview 權杖並自動提供草稿內容
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!entry) {
  return Astro.redirect("/404");
}
---

{isPreview && (
  <div class="preview-banner">
    您正在檢視預覽。此內容尚未發布。
  </div>
)}

<article>
  <h1>{entry.data.title}</h1>
</article>

當透過有效的預覽權杖提供草稿內容時,isPreview 旗標為 true

產生預覽 URL

使用 getPreviewUrl() 建立預覽連結。該函式將密鑰作為顯式參數:

import { getPreviewUrl } from "emdash";

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	expiresIn: "1h",
});
// 傳回:/posts/my-draft-post?_preview=eyJjaWQ...

EMDASH_PREVIEW_SECRET 未設定時,EmDash 會自動產生並在資料庫中儲存每個網站的密鑰以進行權杖驗證。getPreviewUrl() 範本輔助程式仍然要求您顯式傳遞密鑰 — 如果從頁面範本呼叫它,請固定您的環境變數。大多數網站使用管理介面的「產生預覽連結」按鈕,該按鈕透過 API 進行,並自動使用已解析的密鑰。

傳遞 baseUrl 以產生絕對 URL:

const fullUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	baseUrl: "https://example.com",
});
// 傳回:https://example.com/posts/my-draft-post?_preview=eyJjaWQ...

傳遞 pathPattern 以使用自訂路徑產生 URL:

const blogUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	pathPattern: "/blog/{id}",
});
// 傳回:/blog/my-draft-post?_preview=eyJjaWQ...

區域感知路徑

pathPattern 還支援 {locale} 佔位符。當條目位於預設區域設定且 prefixDefaultLocalefalse 時,傳遞空的 locale;空值留下的相鄰斜線會自動摺疊。

以下範例建構帶有區域設定前綴的預覽 URL:

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "pt-br",
});
// 傳回:/pt-br/hello?_preview=...

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "", // 預設區域設定,無前綴
});
// 傳回:/hello?_preview=...

管理員的「在網站上檢視」連結透過 POST /_emdash/api/content/{collection}/{id}/preview-url,它讀取條目的區域設定,查詢網站的 i18n 組態並自動提供 locale。要變更該端點使用的預設模式,請設定 EMDASH_PREVIEW_PATH_PATTERN(例如 /{locale}/{id})— 當請求主體包含自己的 pathPattern 時,請求主體仍然優先。

權杖過期

控制預覽連結保持有效的時長:

// 1小時有效(預設)
await getPreviewUrl({ ..., expiresIn: "1h" });

// 30分鐘有效
await getPreviewUrl({ ..., expiresIn: "30m" });

// 1天有效
await getPreviewUrl({ ..., expiresIn: "1d" });

// 2週有效
await getPreviewUrl({ ..., expiresIn: "2w" });

// 3600秒有效
await getPreviewUrl({ ..., expiresIn: 3600 });

支援的單位:s(秒)、m(分鐘)、h(小時)、d(天)、w(週)。

驗證權杖

使用 verifyPreviewToken() 驗證傳入的預覽請求:

import { verifyPreviewToken } from "emdash";

// 從 URL(提取 _preview 查詢參數)
const result = await verifyPreviewToken({
	url: Astro.url,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});

// 或直接使用權杖
const result = await verifyPreviewToken({
	token: someTokenString,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});

結果指示權杖是否有效:

if (result.valid) {
	// 權杖有效
	console.log(result.payload.cid); // "posts:my-draft-post"
	console.log(result.payload.exp); // 過期時間戳記
	console.log(result.payload.iat); // 簽發時間戳記
} else {
	// 權杖無效
	console.log(result.error);
	// "none" - 沒有權杖
	// "malformed" - 權杖結構無效
	// "invalid" - 簽章驗證失敗
	// "expired" - 權杖已過期
}

預覽指示器

您可以在預覽內容時顯示可視指示器。getEmDashEntry 傳回的 isPreview 旗標告訴您何時正在提供草稿內容:

{isPreview && (
  <div class="preview-banner" role="alert">
    <strong>預覽</strong> — 您正在檢視未發布的內容。
    <a href={Astro.url.pathname}>退出預覽</a>
  </div>
)}

輔助函式

isPreviewRequest(url)

檢查 URL 是否包含預覽權杖:

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
	// 處理預覽請求
}

getPreviewToken(url)

從 URL 中提取權杖字串:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// 傳回權杖字串或 null

parseContentId(contentId)

將內容 ID 解析為集合和 ID:

import { parseContentId } from "emdash";

const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }

權杖安全性

預覽權杖已簽署且有時間限制。CLI 和輔助函式為您產生和驗證它們;您不需要手動建構或解析它們。權杖標識一個條目,並在過期後停止運作。

完整範例

以下頁面在完整的部落格文章範本中結合了預覽和視覺化編輯支援:

---
import { getEmDashEntry } from "emdash";
import BaseLayout from "../../layouts/Base.astro";
import { PortableText } from "emdash/ui";

const { slug } = Astro.params;

// 預覽是自動的 — 中介軟體處理權杖驗證
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!entry) {
  return Astro.redirect("/404");
}
---

<BaseLayout title={entry.data.title}>
  {isPreview && (
    <div class="preview-banner" role="alert">
      <strong>預覽</strong> — 此內容尚未發布。
    </div>
  )}

  <article {...entry.edit}>
    <header>
      <h1 {...entry.edit.title}>{entry.data.title}</h1>
      {entry.data.publishedAt && (
        <time datetime={entry.data.publishedAt.toISOString()}>
          {entry.data.publishedAt.toLocaleDateString()}
        </time>
      )}
      {isPreview && !entry.data.publishedAt && (
        <span class="draft-indicator">草稿</span>
      )}
    </header>

    <div class="content" {...entry.edit.content}>
      <PortableText value={entry.data.content} />
    </div>
  </article>
</BaseLayout>

注意 {...entry.edit}{...entry.edit.title} 展開 — 這些為經過身分驗證的編輯者新增 data-emdash-ref 屬性以啟用視覺化編輯。在生產環境中,它們不產生輸出。

API 參考

getPreviewUrl(options)

使用簽署權杖產生預覽 URL。

選項:

  • collection — 集合 slug(字串)
  • id — 內容 ID 或 slug(字串)
  • secret — 簽署密鑰(字串)
  • expiresIn — 權杖有效期(預設:"1h"
  • baseUrl — 絕對連結的可選基礎 URL
  • pathPattern — 帶有 {collection}{id}{locale} 佔位符的 URL 模式(預設:"/{collection}/{id}"
  • locale — 替換 {locale} 的值。空字串省略區域設定段(斜線被摺疊)。

傳回: Promise<string>

verifyPreviewToken(options)

驗證預覽權杖。

選項:

  • secret — 驗證密鑰(字串)
  • url — 用於提取權杖的 URL,或
  • token — 直接的權杖字串

傳回: Promise<VerifyPreviewTokenResult>

type VerifyPreviewTokenResult =
	| { valid: true; payload: PreviewTokenPayload }
	| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };

generatePreviewToken(options)

產生權杖而不建構 URL。

選項:

  • contentId — 格式為 collection:id 的內容 ID
  • expiresIn — 權杖有效期(預設:"1h"
  • secret — 簽署密鑰

傳回: Promise<string>