EmDash 的預覽系統讓編輯者可以透過安全、有時限的 URL 檢視未發佈內容。預覽連結使用 HMAC-SHA256 簽署的權杖,你可以與審稿者分享,而不必暴露整份草稿。
運作方式
- 管理後台為草稿文章產生預覽 URL
- URL 包含帶過期時間的已簽署
_preview查詢參數 - EmDash 中介軟體自動驗證權杖並設定請求內容
- 你的範本照常呼叫
getEmDashEntry()— 草稿會自動提供
預覽是隱含的。範本不必處理權杖或傳入預覽選項 — 中介軟體與查詢函式透過 AsyncLocalStorage 完成一切。
設定預覽
在環境中加入預覽密鑰:
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",
});
// Returns: /posts/my-draft-post?_preview=eyJjaWQ...
需要絕對連結時指定基底 URL:
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
// Returns: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...
自訂路徑模式:
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
// Returns: /blog/my-draft-post?_preview=eyJjaWQ...
權杖過期
控制預覽連結的有效時間:
// 有效 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" }
權杖格式
預覽權杖採緊湊格式:base64url(payload).base64url(signature)
載荷包含:
cid— 格式為collection:id的內容 IDexp— 過期時間戳(自紀元起的秒數)iat— 簽發時間戳(自紀元起的秒數)
權杖以預覽密鑰透過 HMAC-SHA256 簽署。
完整範例
支援預覽與視覺化編輯的完整部落格文章頁:
---
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(string)id— 內容 ID 或 slug(string)secret— 簽署密鑰(string)expiresIn— 權杖有效期(預設"1h")baseUrl— 選用,絕對連結的基底 URLpathPattern— 含{collection}與{id}預留位置的 URL 模式(預設"/{collection}/{id}")
傳回: Promise<string>
verifyPreviewToken(options)
驗證預覽權杖。
選項:
secret— 驗證密鑰(string)url— 用於擷取權杖的 URL,或token— 權杖字串本身
傳回: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
只產生權杖,不組合 URL。
選項:
contentId— 格式為collection:id的內容 IDexpiresIn— 有效期(預設"1h")secret— 簽署密鑰
傳回: Promise<string>