EmDash의 미리보기 시스템을 통해 편집자는 안전하고 시간 제한이 있는 URL을 통해 미게시 콘텐츠를 볼 수 있습니다. 미리보기 링크는 전체 초안 콘텐츠를 노출하지 않고 검토자와 공유할 수 있는 HMAC-SHA256 서명된 토큰을 사용합니다.
작동 방식
- 관리자가 초안 게시물에 대한 미리보기 URL을 생성합니다
- URL에는 만료 시간이 있는 서명된
_preview쿼리 매개변수가 포함됩니다 - EmDash의 미들웨어가 자동으로 토큰을 확인하고 요청 컨텍스트를 설정합니다
- 템플릿 코드는 평소처럼
getEmDashEntry()를 호출합니다 — 초안 콘텐츠가 자동으로 제공됩니다
미리보기는 암묵적입니다. 미들웨어가 토큰을 확인하고 쿼리 함수가 AsyncLocalStorage를 통해 이를 읽기 때문에 동일한 템플릿 코드가 미리보기 중에는 초안 콘텐츠를, 그렇지 않으면 게시된 콘텐츠를 제공합니다.
미리보기 설정
미리보기는 EmDash가 설치되는 즉시 작동합니다. 처음 사용할 때 EmDash는 사이트별 미리보기 시크릿을 생성하여 데이터베이스에 저장하므로 일반적인 경우 구성이 필요하지 않습니다.
다음이 필요한 경우에만 환경에 EMDASH_PREVIEW_SECRET를 설정하세요:
- 여러 프로세스 간에 시크릿을 공유해야 하는 경우 (예: URL에 서명하여 확인을 위해 메인 사이트로 보내는 별도의 미리보기 Worker)
- 규정 준수/감사 이유로 제어하는 값으로 시크릿을 고정해야 하는 경우
- 백업에서 복원할 때 알려진 값으로 마이그레이션해야 하는 경우
# 선택사항: 자동 생성된 시크릿 재정의
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
설정되면 환경 값이 DB에 저장된 값보다 우선합니다.
기존 템플릿은 다음 페이지처럼 자동으로 미리보기와 작동합니다:
---
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() 템플릿 헬퍼는 여전히 시크릿을 명시적으로 전달할 것을 요구합니다 — 페이지 템플릿에서 호출하는 경우 환경 변수를 고정하세요. 대부분의 사이트는 대신 관리 UI의 “미리보기 링크 생성” 버튼을 사용하며, 이는 API를 통해 이동하고 해결된 시크릿을 자동으로 사용합니다.
절대 URL을 생성하려면 baseUrl을 전달하세요:
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...
사용자 정의 경로로 URL을 생성하려면 pathPattern을 전달하세요:
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} 자리 표시자도 지원합니다. 항목이 기본 로케일에 있고 prefixDefaultLocale이 false인 경우 빈 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— 컬렉션 슬러그 (문자열)id— 콘텐츠 ID 또는 슬러그 (문자열)secret— 서명 시크릿 (문자열)expiresIn— 토큰 유효 기간 (기본값:"1h")baseUrl— 절대 링크용 선택적 기본 URLpathPattern—{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형식의 콘텐츠 IDexpiresIn— 토큰 유효 기간 (기본값:"1h")secret— 서명 시크릿
반환: Promise<string>