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",
});
// 반환: /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",
});
// 반환: 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}",
});
// 반환: /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>