Modo de pré-visualização

Nesta página

O sistema de pré-visualização do EmDash permite que editores vejam conteúdo não publicado por URLs seguras e de duração limitada. Os links usam tokens assinados com HMAC-SHA256 que pode partilhar com revisores sem expor todo o rascunho.

Como funciona

  1. O painel de administração gera um URL de pré-visualização para um rascunho
  2. O URL inclui o parâmetro de consulta assinado _preview com tempo de expiração
  3. O middleware do EmDash verifica o token automaticamente e prepara o contexto do pedido
  4. O seu template chama getEmDashEntry() normalmente: o rascunho é servido automaticamente

A pré-visualização é implícita. Não precisa de tratar tokens nem opções de pré-visualização no template: o middleware e as funções de consulta gerem tudo com AsyncLocalStorage.

Configurar a pré-visualização

Adicione um segredo de pré-visualização ao ambiente:

EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Gere uma cadeia aleatória segura. Esse segredo assina e verifica os tokens.

Pronto. Os seus templates existentes funcionam automaticamente com a pré-visualização:

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

const { slug } = Astro.params;

// Não é necessária lógica especial: o middleware
// deteta os tokens _preview e serve o rascunho automaticamente
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á a ver uma pré-visualização. Este conteúdo não está publicado.
  </div>
)}

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

O indicador isPreview é true quando é servido rascunho com um token válido.

Gerar URLs de pré-visualização

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",
});

Com URL base para links absolutos:

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

Com padrão de caminho personalizado:

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

Expiração do 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 pré-visualização

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

Formato do token

base64url(payload).base64url(signature)

  • cid — ID de conteúdo collection:id
  • exp — Expiração (segundos desde epoch)
  • iat — Emissão (segundos desde epoch)

Assinado com HMAC-SHA256.

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("Server error", { 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.publishedAt && (
        <span class="draft-indicator">Rascunho</span>
      )}
    </header>

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

Referência da API

getPreviewUrl(options)

  • collection, id, secret, expiresIn (por defeito "1h"), baseUrl, pathPattern (por defeito "/{collection}/{id}")

Devolve: Promise<string>

verifyPreviewToken(options)

  • secret e url ou token

Devolve: Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

  • contentId, expiresIn, secret

Devolve: Promise<string>