El sistema de vista previa de EmDash permite a los editores ver contenido no publicado a través de URLs seguras y con tiempo limitado. Los enlaces de vista previa usan tokens firmados con HMAC-SHA256 que puedes compartir con revisores sin exponer todo tu contenido en borrador.
Cómo funciona
- El admin genera una URL de vista previa para un post en borrador
- La URL contiene un parámetro de consulta
_previewfirmado con un tiempo de expiración - El middleware de EmDash verifica automáticamente el token y configura el contexto de la solicitud
- Tu código de plantilla llama a
getEmDashEntry()como siempre — el contenido en borrador se sirve automáticamente
La vista previa es implícita. El middleware verifica el token y las funciones de consulta lo leen a través de AsyncLocalStorage, por lo que el mismo código de plantilla sirve contenido en borrador durante una vista previa y contenido publicado en otros casos.
Configurar la vista previa
La vista previa funciona tan pronto como EmDash está instalado. En el primer uso, EmDash genera un secreto de vista previa por sitio y lo almacena en la base de datos, por lo que el caso común no necesita configuración.
Establece EMDASH_PREVIEW_SECRET en tu entorno solo si necesitas:
- Compartir el secreto entre múltiples procesos (ej. un Worker de vista previa separado que firma URLs y las envía a tu sitio principal para verificación)
- Fijar el secreto a un valor que controles por razones de cumplimiento/auditoría
- Migrar a un valor conocido al restaurar desde un respaldo
# Opcional: sobrescribir el secreto generado automáticamente
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Si está establecido, el valor del entorno prevalece sobre el valor almacenado en la BD.
Las plantillas existentes funcionan con la vista previa automáticamente, como en la siguiente página:
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
// No se necesita manejo especial de vista previa — el middleware
// detecta tokens _preview y sirve contenido en borrador automáticamente
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Error del servidor", { status: 500 });
}
if (!entry) {
return Astro.redirect("/404");
}
---
{isPreview && (
<div class="preview-banner">
Estás viendo una vista previa. Este contenido no está publicado.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
La bandera isPreview es true cuando se sirve contenido en borrador a través de un token de vista previa válido.
Generar URLs de vista previa
Usa getPreviewUrl() para crear enlaces de vista previa. La función toma el secreto como argumento explícito:
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
expiresIn: "1h",
});
// Retorna: /posts/my-draft-post?_preview=eyJjaWQ...
Cuando EMDASH_PREVIEW_SECRET no está establecido, EmDash auto-genera y almacena un secreto por sitio en la base de datos para la verificación de tokens. El helper de plantilla getPreviewUrl() aún requiere que pases el secreto explícitamente — fija tu variable de entorno si lo llamas desde plantillas de página. La mayoría de los sitios usan el botón “Generar enlace de vista previa” de la UI del admin, que pasa por la API y usa el secreto resuelto automáticamente.
Pasa baseUrl para generar una URL absoluta:
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
// Retorna: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...
Pasa pathPattern para generar una URL con una ruta personalizada:
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
// Retorna: /blog/my-draft-post?_preview=eyJjaWQ...
Rutas con reconocimiento de locale
pathPattern también soporta un marcador {locale}. Pasa un locale vacío cuando la entrada está en el locale predeterminado y prefixDefaultLocale es false; las barras adyacentes dejadas por el valor vacío se colapsan automáticamente.
El siguiente ejemplo construye una URL de vista previa con prefijo de locale:
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "pt-br",
});
// Retorna: /pt-br/hello?_preview=...
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "", // locale predeterminado, sin prefijo
});
// Retorna: /hello?_preview=...
El enlace “Ver en el sitio” del admin pasa por POST /_emdash/api/content/{collection}/{id}/preview-url, que lee el locale de la entrada, busca la configuración i18n del sitio y suministra el locale automáticamente. Para cambiar el patrón predeterminado usado por ese endpoint, establece EMDASH_PREVIEW_PATH_PATTERN (ej. /{locale}/{id}) — los cuerpos de solicitud aún prevalecen cuando incluyen su propio pathPattern.
Expiración del token
Controla cuánto tiempo los enlaces de vista previa permanecen válidos:
// Válido por 1 hora (predeterminado)
await getPreviewUrl({ ..., expiresIn: "1h" });
// Válido por 30 minutos
await getPreviewUrl({ ..., expiresIn: "30m" });
// Válido por 1 día
await getPreviewUrl({ ..., expiresIn: "1d" });
// Válido por 2 semanas
await getPreviewUrl({ ..., expiresIn: "2w" });
// Válido por 3600 segundos
await getPreviewUrl({ ..., expiresIn: 3600 });
Unidades soportadas: s (segundos), m (minutos), h (horas), d (días), w (semanas).
Verificar tokens
Usa verifyPreviewToken() para validar solicitudes de vista previa entrantes:
import { verifyPreviewToken } from "emdash";
// Desde una URL (extrae el parámetro de consulta _preview)
const result = await verifyPreviewToken({
url: Astro.url,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
// O con un token directamente
const result = await verifyPreviewToken({
token: someTokenString,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
El resultado indica si el token es válido:
if (result.valid) {
// El token es válido
console.log(result.payload.cid); // "posts:my-draft-post"
console.log(result.payload.exp); // Marca de tiempo de expiración
console.log(result.payload.iat); // Marca de tiempo de emisión
} else {
// El token es inválido
console.log(result.error);
// "none" - no hay token presente
// "malformed" - la estructura del token es inválida
// "invalid" - la verificación de firma falló
// "expired" - el token ha expirado
}
Indicador de vista previa
Puedes mostrar un indicador visual cuando el contenido está siendo previsualizado. La bandera isPreview retornada por getEmDashEntry te indica cuando se está sirviendo contenido en borrador:
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Vista previa</strong> — Estás viendo contenido no publicado.
<a href={Astro.url.pathname}>Salir de vista previa</a>
</div>
)}
Funciones auxiliares
isPreviewRequest(url)
Verificar si una URL contiene un token de vista previa:
import { isPreviewRequest } from "emdash";
if (isPreviewRequest(Astro.url)) {
// Manejar solicitud de vista previa
}
getPreviewToken(url)
Extraer el string del token de una URL:
import { getPreviewToken } from "emdash";
const token = getPreviewToken(Astro.url);
// Retorna el string del token o null
parseContentId(contentId)
Parsear un ID de contenido en colección e ID:
import { parseContentId } from "emdash";
const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }
Seguridad del token
Los tokens de vista previa están firmados y tienen tiempo limitado. La CLI y las funciones auxiliares los generan y verifican por ti; no los construyes ni los parseas manualmente. Un token identifica una entrada y deja de funcionar después de que expira.
Ejemplo completo
La siguiente página combina soporte de vista previa y edición visual en una plantilla completa de post de blog:
---
import { getEmDashEntry } from "emdash";
import BaseLayout from "../../layouts/Base.astro";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
// La vista previa es automática — el middleware maneja la verificación del token
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Error del servidor", { 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.status === "draft" && (
<span class="draft-indicator">Borrador</span>
)}
</header>
<div class="content" {...entry.edit.content}>
<PortableText value={entry.data.content} />
</div>
</article>
</BaseLayout>
Nota los spreads {...entry.edit} y {...entry.edit.title} — estos agregan atributos data-emdash-ref que habilitan la edición visual para editores autenticados. En producción, no producen salida.
Referencia de API
getPreviewUrl(options)
Genera una URL de vista previa con un token firmado.
Opciones:
collection— Slug de la colección (string)id— ID de contenido o slug (string)secret— Secreto de firma (string)expiresIn— Duración de validez del token (predeterminado:"1h")baseUrl— URL base opcional para enlaces absolutospathPattern— Patrón de URL con marcadores{collection},{id}y{locale}(predeterminado:"/{collection}/{id}")locale— Valor sustituido por{locale}. Un string vacío omite el segmento de locale (las barras se colapsan).
Retorna: Promise<string>
verifyPreviewToken(options)
Verifica un token de vista previa.
Opciones:
secret— Secreto de verificación (string)url— URL de la cual extraer el token, Otoken— String del token directamente
Retorna: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
Genera un token sin construir una URL.
Opciones:
contentId— ID de contenido en formatocollection:idexpiresIn— Duración de validez del token (predeterminado:"1h")secret— Secreto de firma
Retorna: Promise<string>