O sistema de pré-visualização do EmDash permite que editores vejam conteúdo não publicado por URLs seguras e de duração limitada. Os links usam tokens assinados com HMAC-SHA256 que pode partilhar com revisores sem expor todo o rascunho.
Como funciona
- O painel de administração gera um URL de pré-visualização para um rascunho
- O URL inclui o parâmetro de consulta assinado
_previewcom tempo de expiração - O middleware do EmDash verifica o token automaticamente e prepara o contexto do pedido
- O seu template chama
getEmDashEntry()normalmente: o rascunho é servido automaticamente
A pré-visualização é implícita. Não precisa de tratar tokens nem opções de pré-visualização no template: o middleware e as funções de consulta gerem tudo com AsyncLocalStorage.
Configurar a pré-visualização
Adicione um segredo de pré-visualização ao ambiente:
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Gere uma cadeia aleatória segura. Esse segredo assina e verifica os tokens.
Pronto. Os seus templates existentes funcionam automaticamente com a pré-visualização:
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
// Não é necessária lógica especial: o middleware
// deteta os tokens _preview e serve o rascunho automaticamente
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">
Está a ver uma pré-visualização. Este conteúdo não está publicado.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
O indicador isPreview é true quando é servido rascunho com um token válido.
Gerar URLs de pré-visualização
Use getPreviewUrl():
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
expiresIn: "1h",
});
Com URL base para links absolutos:
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
Com padrão de caminho personalizado:
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
Expiração do token
await getPreviewUrl({ ..., expiresIn: "1h" });
await getPreviewUrl({ ..., expiresIn: "30m" });
await getPreviewUrl({ ..., expiresIn: "1d" });
await getPreviewUrl({ ..., expiresIn: "2w" });
await getPreviewUrl({ ..., expiresIn: 3600 });
Unidades: s, m, h, d, w.
Verificar tokens
Use verifyPreviewToken():
import { verifyPreviewToken } from "emdash";
const result = await verifyPreviewToken({
url: Astro.url,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
const result2 = await verifyPreviewToken({
token: someTokenString,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
if (result.valid) {
console.log(result.payload.cid);
console.log(result.payload.exp);
console.log(result.payload.iat);
} else {
console.log(result.error);
}
Indicador de pré-visualização
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Pré-visualização</strong> — Está a ver conteúdo não publicado.
<a href={Astro.url.pathname}>Sair da pré-visualização</a>
</div>
)}
Funções auxiliares
isPreviewRequest(url)
import { isPreviewRequest } from "emdash";
if (isPreviewRequest(Astro.url)) {
}
getPreviewToken(url)
import { getPreviewToken } from "emdash";
const token = getPreviewToken(Astro.url);
parseContentId(contentId)
import { parseContentId } from "emdash";
const { collection, id } = parseContentId("posts:my-draft-post");
Formato do token
base64url(payload).base64url(signature)
cid— ID de conteúdocollection:idexp— Expiração (segundos desde epoch)iat— Emissão (segundos desde epoch)
Assinado com HMAC-SHA256.
Exemplo completo
---
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>Pré-visualização</strong> — Este conteúdo não está publicado.
</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">Rascunho</span>
)}
</header>
<div class="content" {...entry.edit.content}>
<PortableText value={entry.data.content} />
</div>
</article>
</BaseLayout>
Referência da API
getPreviewUrl(options)
collection,id,secret,expiresIn(por defeito"1h"),baseUrl,pathPattern(por defeito"/{collection}/{id}")
Devolve: Promise<string>
verifyPreviewToken(options)
secreteurloutoken
Devolve: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
contentId,expiresIn,secret
Devolve: Promise<string>