プレビューモード
このページで分かること
- 署名付きで有効期限のあるプレビューURLの仕組みと、ミドルウェアが下書きを返す流れ
- プレビュー用の秘密鍵(
EMDASH_PREVIEW_SECRET)を設定する必要がある場合と、getPreviewUrl()でURLを作る方法(ロケール付きのパスを含む) - 有効期限の設定、独自の処理でのトークンの検証、プレビュー中であることの表示、関連する関数のリファレンス
このページの目次
EmDashのプレビューの仕組みを使うと、編集者は安全で有効期限のあるURLから、未公開のコンテンツを確認できます。プレビューリンクはHMAC-SHA256で署名したトークンを使うため、下書きのコンテンツ全体を公開することなく、確認担当者と共有できます。
プレビューのリクエストの流れ
- 管理者が、下書きの投稿のプレビューURLを作成します
- URLには、有効期限付きで署名された
_previewクエリパラメーターが含まれます - EmDashのミドルウェアが自動的にトークンを検証し、リクエストのコンテキストを設定します
- テンプレートのコードは通常どおり
getEmDashEntry()を呼び出します。下書きのコンテンツは自動的に返されます
ミドルウェアは、ページを描画する前にトークンを検証し、そのトークンがどのコンテンツのエントリーを許可しているかを記録します。getEmDashEntry() は、そのリクエストのコンテキストを自動的に読み取ります。そのため、同じテンプレートが、有効で対象の一致するプレビューリンクには下書きを返し、通常のリクエストには公開済みのエントリーを返します。別のコレクションやエントリー向けのトークンで、リクエストされたページの下書きにアクセスできることはありません。
やさしい解説
WordPressの「プレビュー」と同じく、公開前の記事を確認するための仕組みです。EmDashでは、プレビュー用のURLに署名付きの _preview パラメーターが付き、そのURLを開いたときだけ下書きが表示されます。テンプレートの側で下書きかどうかを判定するコードを書く必要はなく、通常どおり getEmDashEntry() を呼び出すだけです。トークンには有効期限があり、1つのエントリーにしか使えないため、リンクを共有しても他の下書きは見られません。
プレビューの設定
プレビューは、EmDashをインストールした時点で使えます。初めて使うときに、EmDashはサイトごとのプレビュー用の秘密鍵を生成してデータベースに保存するため、一般的な場合は設定が要りません。
次の場合にだけ、環境変数に EMDASH_PREVIEW_SECRET を設定します。
- 複数のプロセスで秘密鍵を共有する必要がある場合(例:URLに署名してメインのサイトに送り、メインのサイトで検証させる、別のプレビュー用Worker)
- コンプライアンスや監査の理由で、秘密鍵を自分で管理する値に固定する場合
- バックアップから復元するときに、既知の値に移行する場合
# Optional: override the auto-generated secret
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
設定した場合は、データベースに保存された値より環境変数の値が優先されます。
既存のテンプレートは、次のページのように、そのままプレビューに対応します。
---
import { getEmDashEntry } from "emdash";
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");
}
---
{isPreview && (
<div class="preview-banner">
You are viewing the preview version of this page.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
isPreview フラグは、有効なプレビュートークンが、クエリで返されたエントリーと一致するときに true になります。無効なトークン、期限切れのトークン、一致しないトークンでは、下書きのコンテンツは表示されません。
やさしい解説
多くのサイトでは、プレビューのための設定は何も要りません。秘密鍵(プレビューのURLに署名するための値)は、EmDashが初回に自動で作り、データベースに保存します。EMDASH_PREVIEW_SECRET を自分で設定するのは、このページに挙げられた3つの場合(複数のプロセスで共有する、値を固定する、バックアップからの復元で既知の値に移す)だけです。
プレビューURLの作成
ほとんどの編集者は、管理画面の「View on site」を使います。この操作はプレビューURLのエンドポイントを呼び出し、エンドポイントはブラウザーに秘密鍵を渡さずにサイトの秘密鍵を解決します。
サーバー側のアプリケーションのコードでリンクを作る必要がある場合は、getPreviewUrl() を使います。このヘルパーには、署名用の秘密鍵を明示的に渡す必要があります。秘密鍵は process.env から読み取り、変数がない場合はリンクを作る前にエラーにします。
import { getPreviewUrl } from "emdash";
const secret = process.env.EMDASH_PREVIEW_SECRET;
if (!secret) throw new Error("EMDASH_PREVIEW_SECRET is required");
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret,
expiresIn: "1h",
});
// Returns: /posts/my-draft-post?_preview=eyJjaWQ...
EMDASH_PREVIEW_SECRET が設定されていない場合、EmDashのエンドポイントはサイトごとの秘密鍵を生成してデータベースに保存します。単体で使うヘルパーはこの保存された値を読み取れないため、アプリケーションのコードからヘルパーを呼び出す場合は環境変数を設定します。
id の値は、許可するコンテンツを識別し、URLのパスの {id} にも置き換えられます。データベースのIDとスラッグのどちらでも指定できます。移動先のページが getEmDashEntry() に渡すものと同じ識別子を使います。
絶対URLを作るには、baseUrl を渡します。
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret,
baseUrl: "https://example.com",
});
// Returns: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...
独自のパスでURLを作るには、pathPattern を渡します。
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret,
pathPattern: "/blog/{id}",
});
// Returns: /blog/my-draft-post?_preview=eyJjaWQ...
ロケールに対応したパス
pathPattern は {locale} のプレースホルダーにも対応しています。エントリーが既定のロケールで、prefixDefaultLocale が false の場合は、空の locale を渡します。空の値によって連続したスラッシュは、自動的に1つにまとめられます。
次の例は、ロケールの接頭辞付きのプレビューURLを作ります。
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "pt-br",
});
// Returns: /pt-br/hello?_preview=...
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "", // default locale, no prefix
});
// Returns: /hello?_preview=...
管理画面の「View on site」のリンクは、POST /_emdash/api/content/{collection}/{id}/preview-url を経由します。このエンドポイントはエントリーのデータベースIDを使い、エントリーのロケールを自動的に補います。公開側のルートが既定の /{collection}/{id} のパターンと異なる場合は、EMDASH_PREVIEW_PATH_PATTERN を設定します。たとえば /{locale}/posts/{id} を設定すると、ポルトガル語のエントリーでは /pt-br/posts/01ABC...、接頭辞のない既定のロケールのエントリーでは /posts/01ABC... が作られます。移動先のルートは、そのデータベースIDを getEmDashEntry() に渡せます。リクエストの本文に pathPattern があれば、環境変数の値より優先されます。
このエンドポイントは、エントリーのデータベースIDとは別にスラッグを差し込むことはできません。プレビューURLにスラッグを使う必要がある場合は、サーバー側のアプリケーションのコードで getPreviewUrl() を使って作り、スラッグを id として渡します。
{locale} のプレースホルダーには、設定したロケールのコードが入ります。Astroの独自のロケールの path の対応づけは適用されません。公開側のルートが別のセグメントを使う場合は、そのセグメントを指定してヘルパーを呼び出すか、管理画面のエンドポイントが使うロケールのコードを受け付けるルートを用意します。
トークンの有効期限の設定
プレビューリンクが有効な期間を指定します。
const preview = {
collection: "posts",
id: "my-draft-post",
secret,
};
// Valid for 1 hour (default)
await getPreviewUrl(preview);
// Valid for 30 minutes
await getPreviewUrl({ ...preview, expiresIn: "30m" });
// Valid for 1 day
await getPreviewUrl({ ...preview, expiresIn: "1d" });
// Valid for 2 weeks
await getPreviewUrl({ ...preview, expiresIn: "2w" });
// Valid for 3600 seconds
await getPreviewUrl({ ...preview, expiresIn: 3600 });
使える単位:s(秒)、m(分)、h(時間)、d(日)、w(週)。
独自の処理でのトークンの検証
通常のAstroのページでは verifyPreviewToken() を呼び出しません。EmDashのミドルウェアがすでに _preview を検証しているためです。このヘルパーは、ミドルウェアの外にあるコードでトークンを検証する必要がある場合にだけ使います。
import { verifyPreviewToken } from "emdash";
// From a URL (extracts _preview query parameter)
const fromUrl = await verifyPreviewToken({
url: Astro.url,
secret,
});
// Or with a token directly
const fromToken = await verifyPreviewToken({
token: Astro.url.searchParams.get("_preview"),
secret,
});
結果は、トークンが有効かどうかを示します。
if (fromUrl.valid) {
// Token is valid
console.log(fromUrl.payload.cid); // "posts:my-draft-post"
console.log(fromUrl.payload.exp); // Expiry timestamp
console.log(fromUrl.payload.iat); // Issued-at timestamp
} else {
// Token is invalid
console.log(fromUrl.error);
// "none" - no token present
// "malformed" - token structure is invalid
// "invalid" - signature verification failed
// "expired" - token has expired
}
プレビュー中であることの表示
getEmDashEntry が返す isPreview フラグを使って、ページがプレビューのリクエストによるものであることを表示します。
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Preview</strong> — You are viewing the preview version of this page.
<a href={Astro.url.pathname}>Exit preview</a>
</div>
)}
プレビューURLの確認
isPreviewRequest(url)
URLに _preview パラメーターが含まれているかどうかを確認します。トークンの検証はしません。
import { isPreviewRequest } from "emdash";
if (isPreviewRequest(Astro.url)) {
// Handle preview request
}
getPreviewToken(url)
URLからトークンの文字列を取り出します。
import { getPreviewToken } from "emdash";
const token = getPreviewToken(Astro.url);
// Returns the token string or null
parseContentId(contentId)
コンテンツIDを、コレクションとIDに分解します。
import { parseContentId } from "emdash";
const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }
トークンの安全性
プレビュートークンは署名されていて、有効期限があります。管理画面のエンドポイントとヘルパー関数がトークンを作成・検証するため、手作業でトークンを組み立てたり解析したりする必要はありません。1つのトークンは1つのエントリーを識別し、有効期限が過ぎると使えなくなります。
完全な例
次のページは、ブログ記事のテンプレート全体で、プレビューとビジュアル編集への対応を組み合わせています。
---
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>Preview</strong> — You are viewing the preview version of this page.
</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.status === "draft" && (
<span class="draft-indicator">Draft</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}に入る値。空文字列の場合はロケールのセグメントを省略します(スラッシュは1つにまとめられます)。
戻り値: 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>