미리보기 모드

이 페이지

EmDash 미리보기는 편집자가 미공개 콘텐츠를 안전하고 기한이 있는 URL로 확인할 수 있게 합니다. 링크는 HMAC-SHA256으로 서명된 토큰을 사용하므로 초안 전체를 노출하지 않고 검토자와 공유할 수 있습니다.

동작 방식

  1. 관리자가 초안 글용 미리보기 URL을 생성합니다
  2. URL에 만료 시간이 포함된 서명된 _preview 쿼리 매개변수가 붙습니다
  3. EmDash 미들웨어가 토큰을 자동으로 검증하고 요청 컨텍스트를 설정합니다
  4. 템플릿은 평소처럼 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)

페이로드에는 다음이 포함됩니다:

  • cidcollection:id 형식의 콘텐츠 ID
  • exp — 만료 타임스탬프(에포크 이후 초)
  • 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 — 절대 링크를 위한 선택적 기본 URL
  • pathPattern{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을 만들지 않고 토큰만 생성합니다.

옵션:

  • contentIdcollection:id 형식의 콘텐츠 ID
  • expiresIn — 토큰 유효 기간 (기본값: "1h")
  • secret — 서명 시크릿

반환: Promise<string>