Modo de Vista Previa

En esta página

El sistema de vista previa de EmDash permite a los editores ver contenido no publicado a través de URLs seguras con límite de tiempo. Los enlaces de vista previa utilizan tokens firmados con HMAC-SHA256 que puedes compartir con revisores sin exponer todo tu contenido de borrador.

Cómo Funciona

  1. El administrador genera una URL de vista previa para una publicación de borrador
  2. La URL contiene un parámetro de consulta _preview firmado con un tiempo de expiración
  3. El middleware de EmDash verifica automáticamente el token y configura el contexto de la solicitud
  4. Tu código de plantilla llama a getEmDashEntry() como de costumbre — el contenido de 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 de borrador durante una vista previa y contenido publicado en otros casos.

Configuración de la Vista Previa

La vista previa funciona tan pronto como se instala EmDash. 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 (por ejemplo, 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 una copia de seguridad
# Opcional: anula el secreto generado automáticamente
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Si se establece, el valor del entorno tiene prioridad 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 de 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á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 está sirviendo contenido de borrador a través de un token de vista previa válido.

Generación de 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",
});
// Devuelve: /posts/my-draft-post?_preview=eyJjaWQ...

Cuando EMDASH_PREVIEW_SECRET no está establecido, EmDash genera y almacena automáticamente 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 interfaz de administración en su lugar, 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",
});
// Devuelve: 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}",
});
// Devuelve: /blog/my-draft-post?_preview=eyJjaWQ...

Rutas conscientes de locale

pathPattern también admite un marcador de posición {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",
});
// Devuelve: /pt-br/hello?_preview=...

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "", // locale predeterminado, sin prefijo
});
// Devuelve: /hello?_preview=...

El enlace “Ver en el sitio” del administrador 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 proporciona el locale automáticamente. Para cambiar el patrón predeterminado usado por ese endpoint, establece EMDASH_PREVIEW_PATH_PATTERN (por ejemplo /{locale}/{id}) — los cuerpos de solicitud aún ganan cuando incluyen su propio pathPattern.

Expiración de Tokens

Controla cuánto tiempo permanecen válidos los enlaces de vista previa:

// 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 admitidas: s (segundos), m (minutos), h (horas), d (días), w (semanas).

Verificación de 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 se está previsualizando contenido. La bandera isPreview devuelta por getEmDashEntry te indica cuándo se está sirviendo contenido de 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 de Ayuda

isPreviewRequest(url)

Verifica si una URL contiene un token de vista previa:

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
	// Manejar solicitud de vista previa
}

getPreviewToken(url)

Extrae la cadena de token de una URL:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Devuelve la cadena de token o null

parseContentId(contentId)

Analiza 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 de Tokens

Los tokens de vista previa están firmados y tienen límite de tiempo. La CLI y las funciones de ayuda los generan y verifican por ti; no los construyes ni analizas 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 publicación 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 de tokens
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>

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 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 absolutos
  • pathPattern — Patrón de URL con marcadores de posición {collection}, {id} y {locale} (predeterminado: "/{collection}/{id}")
  • locale — Valor sustituido por {locale}. Cadena vacía omite el segmento de locale (las barras se colapsan).

Devuelve: Promise<string>

verifyPreviewToken(options)

Verifica un token de vista previa.

Opciones:

  • secret — Secreto de verificación (string)
  • url — URL para extraer el token, O
  • token — Cadena de token directamente

Devuelve: 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 formato collection:id
  • expiresIn — Duración de validez del token (predeterminado: "1h")
  • secret — Secreto de firma

Devuelve: Promise<string>