El sistema de vista previa de EmDash permite a las personas editoras ver contenido no publicado mediante URLs seguras y de duración limitada. Los enlaces usan tokens firmados con HMAC-SHA256 que puede compartir con revisoras y revisores sin exponer todo el borrador.
Cómo funciona
- El panel de administración genera una URL de vista previa para un borrador
- La URL incluye el parámetro de consulta firmado
_previewcon tiempo de caducidad - El middleware de EmDash verifica el token automáticamente y prepara el contexto de la petición
- Su plantilla llama a
getEmDashEntry()con normalidad: el borrador se sirve automáticamente
La vista previa es implícita. No necesita manejar tokens ni opciones de vista previa en la plantilla: el middleware y las funciones de consulta lo gestionan con AsyncLocalStorage.
Configurar la vista previa
Añada un secreto de vista previa al entorno:
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Genere una cadena aleatoria segura. Ese secreto firma y verifica los tokens.
Listo. Sus plantillas existentes funcionan con la vista previa automáticamente:
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
// No hace falta lógica especial: el middleware
// detecta los tokens _preview y sirve el borrador automáticamente
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á viendo una vista previa. Este contenido no está publicado.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
El indicador isPreview es true cuando se sirve borrador con un token válido.
Generar URLs de vista previa
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",
});
Con URL base para enlaces absolutos:
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
Con patrón de ruta personalizado:
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
Caducidad del 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 vista previa
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Vista previa</strong> — Está viendo contenido no publicado.
<a href={Astro.url.pathname}>Salir de la vista previa</a>
</div>
)}
Funciones 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 del token
base64url(payload).base64url(signature)
cid— ID de contenidocollection:idexp— Caducidad (segundos desde epoch)iat— Emisión (segundos desde epoch)
Firmado con HMAC-SHA256.
Ejemplo 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>Vista previa</strong> — Este contenido no 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">Borrador</span>
)}
</header>
<div class="content" {...entry.edit.content}>
<PortableText value={entry.data.content} />
</div>
</article>
</BaseLayout>
Referencia de API
getPreviewUrl(options)
collection,id,secret,expiresIn(por defecto"1h"),baseUrl,pathPattern(por defecto"/{collection}/{id}")
Devuelve: Promise<string>
verifyPreviewToken(options)
secretyurlotoken
Devuelve: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
contentId,expiresIn,secret
Devuelve: Promise<string>