Modo de pré-visualização

Nesta página

O sistema de pré-visualização do EmDash permite que editores visualizem conteúdo não publicado através de URLs seguros e com tempo limitado. Links de pré-visualização usam tokens assinados com HMAC-SHA256 que você pode compartilhar com revisores sem expor todo o seu conteúdo rascunho.

Como funciona

  1. O admin gera uma URL de pré-visualização para um post rascunho
  2. A URL contém um parâmetro de consulta _preview assinado com um tempo de expiração
  3. O middleware do EmDash verifica automaticamente o token e configura o contexto da requisição
  4. Seu código de template chama getEmDashEntry() normalmente — conteúdo rascunho é servido automaticamente

A pré-visualização é implícita. O middleware verifica o token e as funções de consulta o leem através do AsyncLocalStorage, de modo que o mesmo código de template serve conteúdo rascunho durante uma pré-visualização e conteúdo publicado caso contrário.

Configurar a pré-visualização

A pré-visualização funciona assim que o EmDash é instalado. No primeiro uso, o EmDash gera um segredo de pré-visualização por site e o armazena no banco de dados, então o caso comum não precisa de configuração.

Defina EMDASH_PREVIEW_SECRET no seu ambiente apenas se precisar:

  • Compartilhar o segredo entre múltiplos processos (ex.: um Worker de pré-visualização separado que assina URLs e as envia ao seu site principal para verificação)
  • Fixar o segredo a um valor que você controle por razões de conformidade/auditoria
  • Migrar para um valor conhecido ao restaurar de um backup
# Opcional: sobrescrever o segredo gerado automaticamente
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Se definido, o valor do ambiente prevalece sobre o valor armazenado no BD.

Templates existentes funcionam automaticamente com a pré-visualização:

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;

const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Erro do servidor", { status: 500 });
}

if (!entry) {
  return Astro.redirect("/404");
}
---

{isPreview && (
  <div class="preview-banner">
    Você está visualizando uma pré-visualização. Este conteúdo não está publicado.
  </div>
)}

<article>
  <h1>{entry.data.title}</h1>
</article>

A flag isPreview é true quando conteúdo rascunho está sendo servido via um token de pré-visualização válido.

Gerar URLs de pré-visualização

Use getPreviewUrl() para criar links de pré-visualização. A função recebe o segredo 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",
});

Quando EMDASH_PREVIEW_SECRET não está definido, o EmDash auto-gera e armazena um segredo por site no banco de dados para verificação de tokens. O helper de template getPreviewUrl() ainda requer que você passe o segredo explicitamente. A maioria dos sites usa o botão “Gerar link de pré-visualização” da UI admin, que passa pela API e usa o segredo resolvido automaticamente.

Passe baseUrl para gerar uma URL absoluta:

const fullUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	baseUrl: "https://example.com",
});

Passe pathPattern para gerar uma URL com caminho personalizado:

const blogUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	pathPattern: "/blog/{id}",
});

Caminhos com reconhecimento de locale

pathPattern também suporta um placeholder {locale}. Passe um locale vazio quando a entrada está no locale padrão e prefixDefaultLocale é false; barras adjacentes deixadas pelo valor vazio são automaticamente colapsadas.

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: "",
});
// Retorna: /hello?_preview=...

O link “Ver no site” do admin passa por POST /_emdash/api/content/{collection}/{id}/preview-url, que lê o locale da entrada, busca a configuração i18n do site e fornece o locale automaticamente. Para alterar o padrão usado por esse endpoint, defina EMDASH_PREVIEW_PATH_PATTERN (ex.: /{locale}/{id}).

Expiração do token

Controle por quanto tempo os links de pré-visualização permanecem válidos:

await getPreviewUrl({ ..., expiresIn: "1h" });  // 1 hora (padrão)
await getPreviewUrl({ ..., expiresIn: "30m" }); // 30 minutos
await getPreviewUrl({ ..., expiresIn: "1d" });  // 1 dia
await getPreviewUrl({ ..., expiresIn: "2w" });  // 2 semanas
await getPreviewUrl({ ..., expiresIn: 3600 });  // 3600 segundos

Unidades suportadas: s (segundos), m (minutos), h (horas), d (dias), w (semanas).

Verificar tokens

Use verifyPreviewToken() para validar requisições de pré-visualização recebidas:

import { verifyPreviewToken } from "emdash";

const result = await verifyPreviewToken({
	url: Astro.url,
	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);
	// "none" | "malformed" | "invalid" | "expired"
}

Indicador de pré-visualização

{isPreview && (
  <div class="preview-banner" role="alert">
    <strong>Pré-visualização</strong> — Você está vendo 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");

Segurança dos tokens

Os tokens de pré-visualização são assinados e com tempo limitado. A CLI e as funções auxiliares os geram e verificam para você. Não os construa ou parse manualmente. Um token identifica uma entrada e para de funcionar após expirar.

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("Erro do servidor", { 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.status === "draft" && (
        <span class="draft-indicator">Rascunho</span>
      )}
    </header>
    <div class="content" {...entry.edit.content}>
      <PortableText value={entry.data.content} />
    </div>
  </article>
</BaseLayout>

Note os spreads {...entry.edit} e {...entry.edit.title} — eles adicionam atributos data-emdash-ref que habilitam edição visual para editores autenticados. Em produção, não produzem saída.

Referência da API

getPreviewUrl(options)

  • collection — Slug da coleção (string)
  • id — ID do conteúdo ou slug (string)
  • secret — Segredo de assinatura (string)
  • expiresIn — Duração de validade do token (padrão: "1h")
  • baseUrl — URL base opcional para links absolutos
  • pathPattern — Padrão de URL com placeholders {collection}, {id} e {locale} (padrão: "/{collection}/{id}")
  • locale — Valor substituído por {locale}. String vazio omite o segmento de locale.

Retorna: Promise<string>

verifyPreviewToken(options)

  • secret — Segredo de verificação (string)
  • url — URL da qual extrair o token, OU
  • token — String do token diretamente

Retorna: Promise<VerifyPreviewTokenResult>

type VerifyPreviewTokenResult =
	| { valid: true; payload: PreviewTokenPayload }
	| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };

generatePreviewToken(options)

  • contentId — ID do conteúdo no formato collection:id
  • expiresIn — Duração de validade do token (padrão: "1h")
  • secret — Segredo de assinatura

Retorna: Promise<string>