Modo de Pré-visualização

Nesta página

O sistema de pré-visualização do EmDash permite que os editores vejam conteúdo não publicado por meio de URLs seguras e com tempo limitado. Os links de pré-visualização usam tokens assinados com HMAC-SHA256 que você pode compartilhar com revisores sem expor todo o seu conteúdo em rascunho.

Como funcionam as requisições de pré-visualização

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

O middleware verifica o token antes de renderizar a página e registra qual entrada de conteúdo ele autoriza. O getEmDashEntry() lê esse contexto da requisição automaticamente. Por isso, o mesmo template serve o rascunho para um link de pré-visualização válido e correspondente e a entrada publicada para uma requisição comum. Um token de outra coleção ou de outra entrada nunca concede acesso ao rascunho da página solicitada.

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, de modo que o caso comum não exige nenhuma configuração.

Defina EMDASH_PREVIEW_SECRET no seu ambiente somente se precisar:

  • Compartilhar o segredo entre vários processos (por exemplo, um Worker de pré-visualização separado que assina as URLs e as envia ao seu site principal para verificação)
  • Fixar o segredo em um valor que você controla por motivos de conformidade ou auditoria
  • Migrar para um valor conhecido ao restaurar a partir de um backup
# Optional: override the auto-generated secret
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Se definido, o valor do ambiente prevalece sobre o valor armazenado no banco de dados.

Os templates existentes funcionam com a pré-visualização automaticamente, como na página a seguir:

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

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.rewrite("/404");
}
---

{isPreview && (
  <div class="preview-banner">
    You are viewing the preview version of this page.
  </div>
)}

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

O sinalizador isPreview é true quando um token de pré-visualização válido corresponde à entrada retornada pela consulta. Tokens inválidos, expirados e não correspondentes não expõem o conteúdo em rascunho.

Gerar URLs de pré-visualização

A maioria dos editores usa View on site no administrador. Essa ação chama o endpoint de URL de pré-visualização, que resolve o segredo do site sem expô-lo ao navegador.

Use getPreviewUrl() quando o código de aplicação no servidor precisar criar um link. O helper exige o segredo de assinatura explicitamente. Leia os segredos de process.env e falhe antes de gerar um link se a variável estiver ausente:

import { getPreviewUrl } from "emdash";

const secret = process.env.EMDASH_PREVIEW_SECRET;
if (!secret) throw new Error("EMDASH_PREVIEW_SECRET is required");

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	expiresIn: "1h",
});
// Returns: /posts/my-draft-post?_preview=eyJjaWQ...

Quando EMDASH_PREVIEW_SECRET não está definido, o endpoint do EmDash gera e armazena no banco de dados um segredo por site. O helper independente não consegue ler esse valor armazenado, portanto configure a variável de ambiente se o código da aplicação chamar o helper.

O valor id identifica o conteúdo autorizado e também substitui {id} no caminho da URL. Pode ser um ID do banco de dados ou um slug. Use o mesmo identificador que a página de destino passa para getEmDashEntry().

Passe baseUrl para gerar uma URL absoluta:

const fullUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	baseUrl: "https://example.com",
});
// Returns: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...

Passe pathPattern para gerar uma URL com um caminho personalizado:

const blogUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	pathPattern: "/blog/{id}",
});
// Returns: /blog/my-draft-post?_preview=eyJjaWQ...

Caminhos com reconhecimento de localidade

pathPattern também oferece suporte a um marcador {locale}. Passe um locale vazio quando a entrada estiver na localidade padrão e prefixDefaultLocale for false; as barras adjacentes deixadas pelo valor vazio são recolhidas automaticamente.

O exemplo a seguir constrói uma URL de pré-visualização com prefixo de localidade:

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "pt-br",
});
// Returns: /pt-br/hello?_preview=...

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "", // default locale, no prefix
});
// Returns: /hello?_preview=...

O link View on site do administrador passa por POST /_emdash/api/content/{collection}/{id}/preview-url. O endpoint usa o ID do banco de dados da entrada e fornece a localidade dela automaticamente. Defina EMDASH_PREVIEW_PATH_PATTERN quando a sua rota pública for diferente do padrão /{collection}/{id}. Por exemplo, /{locale}/posts/{id} produz /pt-br/posts/01ABC... para uma entrada em português e /posts/01ABC... para uma entrada sem prefixo na localidade padrão. A rota de destino pode passar esse ID do banco de dados para getEmDashEntry(). Um pathPattern no corpo da requisição substitui o valor do ambiente.

O endpoint não consegue substituir o slug da entrada separadamente do ID do banco de dados. Se uma URL de pré-visualização precisar usar o slug, gere-a no código de aplicação no servidor com getPreviewUrl() e passe o slug como id.

O marcador {locale} recebe o código de localidade configurado. Ele não aplica os mapeamentos personalizados de path de localidade do Astro. Se a rota pública usar um segmento diferente, chame o helper com esse segmento ou forneça uma rota que aceite o código de localidade usado pelo endpoint do administrador.

Definir a expiração do token

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

const preview = {
	collection: "posts",
	id: "my-draft-post",
	secret,
};

// Valid for 1 hour (default)
await getPreviewUrl(preview);

// Valid for 30 minutes
await getPreviewUrl({ ...preview, expiresIn: "30m" });

// Valid for 1 day
await getPreviewUrl({ ...preview, expiresIn: "1d" });

// Valid for 2 weeks
await getPreviewUrl({ ...preview, expiresIn: "2w" });

// Valid for 3600 seconds
await getPreviewUrl({ ...preview, expiresIn: 3600 });

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

Verificar tokens em um fluxo personalizado

As páginas Astro normais não chamam verifyPreviewToken(); o middleware do EmDash já verifica _preview. Use este helper somente quando o código fora desse middleware precisar validar um token:

import { verifyPreviewToken } from "emdash";

// From a URL (extracts _preview query parameter)
const fromUrl = await verifyPreviewToken({
	url: Astro.url,
	secret,
});

// Or with a token directly
const fromToken = await verifyPreviewToken({
	token: Astro.url.searchParams.get("_preview"),
	secret,
});

O resultado indica se o token é válido:

if (fromUrl.valid) {
	// Token is valid
	console.log(fromUrl.payload.cid); // "posts:my-draft-post"
	console.log(fromUrl.payload.exp); // Expiry timestamp
	console.log(fromUrl.payload.iat); // Issued-at timestamp
} else {
	// Token is invalid
	console.log(fromUrl.error);
	// "none" - no token present
	// "malformed" - token structure is invalid
	// "invalid" - signature verification failed
	// "expired" - token has expired
}

Exibir um indicador de pré-visualização

Use o sinalizador isPreview retornado por getEmDashEntry para mostrar que a página veio de uma requisição de pré-visualização:

{isPreview && (
  <div class="preview-banner" role="alert">
    <strong>Preview</strong> — You are viewing the preview version of this page.
    <a href={Astro.url.pathname}>Exit preview</a>
  </div>
)}

Inspecionar URLs de pré-visualização

isPreviewRequest(url)

Verifica se uma URL contém um parâmetro _preview. Isso não verifica o token:

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
	// Handle preview request
}

getPreviewToken(url)

Extrai a string do token de uma URL:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Returns the token string or null

parseContentId(contentId)

Analisa um ID de conteúdo em coleção e ID:

import { parseContentId } from "emdash";

const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }

Segurança dos tokens

Os tokens de pré-visualização são assinados e têm tempo limitado. O endpoint do administrador e as funções helper os geram e os verificam para você; você não os constrói nem os analisa manualmente. Um token identifica uma entrada e deixa de funcionar quando expira.

Exemplo completo

A página a seguir combina a pré-visualização e o suporte à edição visual em um template completo de post de blog:

---
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.rewrite("/404");
}
---

<BaseLayout title={entry.data.title}>
  {isPreview && (
    <div class="preview-banner" role="alert">
      <strong>Preview</strong> — You are viewing the preview version of this page.
    </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">Draft</span>
      )}
    </header>

    <div class="content" {...entry.edit.content}>
      <PortableText value={entry.data.content} />
    </div>
  </article>
</BaseLayout>

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

Referência da API

getPreviewUrl(options)

Gera uma URL de pré-visualização com um token assinado.

Opções:

  • 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 os marcadores {collection}, {id} e {locale} (padrão: "/{collection}/{id}")
  • locale — Valor que substitui {locale}. Uma string vazia omite o segmento de localidade (as barras são recolhidas).

Retorna: Promise<string>

verifyPreviewToken(options)

Verifica um token de pré-visualização.

Opções:

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

Retorna: Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

Gera um token sem construir uma URL.

Opções:

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

Retorna: Promise<string>